Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pkg/attestation/crafter/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ var RunnerFactories = map[schemaapi.CraftingSchema_Runner_RunnerType]RunnerFacto
schemaapi.CraftingSchema_Runner_GITHUB_ACTION: func(logger *zerolog.Logger) SupportedRunner {
return runners.NewGithubAction(timeoutCtx, logger)
},
schemaapi.CraftingSchema_Runner_GITLAB_PIPELINE: func(_ *zerolog.Logger) SupportedRunner {
return runners.NewGitlabPipeline()
schemaapi.CraftingSchema_Runner_GITLAB_PIPELINE: func(logger *zerolog.Logger) SupportedRunner {
return runners.NewGitlabPipeline(timeoutCtx, logger)
},
schemaapi.CraftingSchema_Runner_AZURE_PIPELINE: func(_ *zerolog.Logger) SupportedRunner {
return runners.NewAzurePipeline()
Expand Down
2 changes: 1 addition & 1 deletion pkg/attestation/crafter/runners/githubaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ func (r *GitHubAction) Environment() RunnerEnvironment {
switch r.githubToken.RunnerEnvironment {
case "github-hosted":
return Managed
case "self-hosted":
case oidc.SelfHostedRunner:
return SelfHosted
default:
return Unknown
Expand Down
36 changes: 32 additions & 4 deletions pkg/attestation/crafter/runners/gitlabpipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,30 @@
package runners

import (
"context"
"os"

schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
"github.com/chainloop-dev/chainloop/pkg/attestation/crafter/runners/oidc"
"github.com/rs/zerolog"
)

type GitlabPipeline struct{}
type GitlabPipeline struct {
gitlabToken *oidc.GitlabToken
}

func NewGitlabPipeline(ctx context.Context, logger *zerolog.Logger) *GitlabPipeline {
client, err := oidc.NewGitlabClient(ctx, logger)
if err != nil {
logger.Debug().Err(err).Msgf("failed to create Gitlab OIDC client: %v", err)
return &GitlabPipeline{
gitlabToken: nil,
}
}

func NewGitlabPipeline() *GitlabPipeline {
return &GitlabPipeline{}
return &GitlabPipeline{
gitlabToken: client.Token,
}
}

func (r *GitlabPipeline) ID() schemaapi.CraftingSchema_Runner_RunnerType {
Expand Down Expand Up @@ -65,13 +80,26 @@ func (r *GitlabPipeline) ResolveEnvVars() (map[string]string, []*error) {
}

func (r *GitlabPipeline) WorkflowFilePath() string {
if r.gitlabToken != nil {
return r.gitlabToken.ConfigRefURI
}
return ""
}

func (r *GitlabPipeline) IsAuthenticated() bool {
return false
return r.gitlabToken != nil
}

func (r *GitlabPipeline) Environment() RunnerEnvironment {
if r.gitlabToken != nil {
switch r.gitlabToken.RunnerEnvironment {
case "gitlab-hosted":
return Managed
case oidc.SelfHostedRunner:
return SelfHosted
default:
return Unknown
}
}
return Unknown
}
5 changes: 4 additions & 1 deletion pkg/attestation/crafter/runners/gitlabpipeline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
package runners

import (
"context"
"os"
"testing"

"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
Expand Down Expand Up @@ -117,7 +119,8 @@ func (s *gitlabPipelineSuite) TestRunnerName() {

// Run before each test
func (s *gitlabPipelineSuite) SetupTest() {
s.runner = NewGitlabPipeline()
logger := zerolog.New(zerolog.Nop()).Level(zerolog.Disabled)
s.runner = NewGitlabPipeline(context.Background(), &logger)
t := s.T()
t.Setenv("GITLAB_CI", "true")
t.Setenv("GITLAB_USER_EMAIL", "foo@foo.com")
Expand Down
107 changes: 107 additions & 0 deletions pkg/attestation/crafter/runners/oidc/gitlab.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//
// Copyright 2025 The Chainloop Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package oidc

import (
"context"
"fmt"
"os"

"github.com/coreos/go-oidc/v3/oidc"
"github.com/rs/zerolog"
)

// GitlabTokenEnv is the environment variable name for Gitlab OIDC token.
// #nosec G101 - This is just the name of an environment variable, not a credential
const GitlabTokenEnv = "GITLAB_OIDC"

// CIServerURLEnv is the environment variable name for Gitlab CI server URL.
const CIServerURLEnv = "CI_SERVER_URL"

type GitlabToken struct {
oidc.IDToken

// ConfigRefURI is a reference to the current job workflow.
ConfigRefURI string `json:"ci_config_ref_uri"`

// RunnerEnvironment is the environment the runner is running in.
RunnerEnvironment string `json:"runner_environment"`
}

type GitlabOIDCClient struct {
Token *GitlabToken
}

func NewGitlabClient(ctx context.Context, logger *zerolog.Logger) (*GitlabOIDCClient, error) {
var c GitlabOIDCClient

// retrieve the Gitlab server on which the pipeline is running, which is the provider URL
providerURL := os.Getenv(CIServerURLEnv)
logger.Debug().Str("providerURL", providerURL).Msg("retrieved provider URL")
if providerURL == "" {
return nil, fmt.Errorf("%s environment variable not set", CIServerURLEnv)
}

tokenContent := os.Getenv(GitlabTokenEnv)
logger.Debug().Msg("retrieved token content")
if tokenContent == "" {
return nil, fmt.Errorf("%s environment variable not set", GitlabTokenEnv)
}

token, err := parseToken(ctx, providerURL, tokenContent)
if err != nil {
return nil, fmt.Errorf("failed to parse token: %w", err)
}

c.Token = token
return &c, nil
}

func parseToken(ctx context.Context, providerURL string, tokenString string) (*GitlabToken, error) {
provider, err := oidc.NewProvider(ctx, providerURL)
if err != nil {
return nil, fmt.Errorf("failed to connect to OIDC provider: %w", err)
}

verifier := provider.Verifier(&oidc.Config{
SkipClientIDCheck: true, // Skip client ID check since we're just parsing
})

idToken, err := verifier.Verify(ctx, tokenString)
if err != nil {
return nil, fmt.Errorf("token verification failed: %w", err)
}

token := &GitlabToken{
IDToken: *idToken,
}

// Extract claims to populate our custom fields
var claims map[string]interface{}
if err := idToken.Claims(&claims); err != nil {
return nil, fmt.Errorf("failed to extract claims: %w", err)
}

if configRefURI, ok := claims["ci_config_ref_uri"].(string); ok {
token.ConfigRefURI = configRefURI
}

if runnerEnv, ok := claims["runner_environment"].(string); ok {
token.RunnerEnvironment = runnerEnv
}

return token, nil
}
83 changes: 83 additions & 0 deletions pkg/attestation/crafter/runners/oidc/gitlab_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//
// Copyright 2025 The Chainloop Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package oidc_test

import (
"context"
"os"
"testing"

"github.com/chainloop-dev/chainloop/pkg/attestation/crafter/runners/oidc"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
)

func TestNewGitlabClient(t *testing.T) {
testLogger := zerolog.New(zerolog.Nop()).Level(zerolog.Disabled)
ctx := context.Background()

// Save original environment variables
originalServerURL := os.Getenv(oidc.CIServerURLEnv)
originalToken := os.Getenv(oidc.GitlabTokenEnv)
defer func() {
t.Setenv(oidc.CIServerURLEnv, originalServerURL)
t.Setenv(oidc.GitlabTokenEnv, originalToken)
}()

tests := []struct {
name string
setupEnv func(t *testing.T)
expectErr bool
expectErrContains string
}{
{
name: "Missing server URL",
setupEnv: func(t *testing.T) {
t.Setenv(oidc.CIServerURLEnv, "")
t.Setenv(oidc.GitlabTokenEnv, "test-token")
},
expectErr: true,
expectErrContains: "environment variable not set",
},
{
name: "Missing OIDC token",
setupEnv: func(t *testing.T) {
t.Setenv(oidc.CIServerURLEnv, "https://gitlab.example.com")
t.Setenv(oidc.GitlabTokenEnv, "")
},
expectErr: true,
expectErrContains: "environment variable not set",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.setupEnv(t)
client, err := oidc.NewGitlabClient(ctx, &testLogger)

if tt.expectErr {
assert.Error(t, err)
if tt.expectErrContains != "" {
assert.Contains(t, err.Error(), tt.expectErrContains)
}
assert.Nil(t, client)
} else {
assert.NoError(t, err)
assert.NotNil(t, client)
}
})
}
}
2 changes: 2 additions & 0 deletions pkg/attestation/crafter/runners/oidc/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"errors"
)

const SelfHostedRunner = "self-hosted"

var (
// errURLError indicates the OIDC server URL is invalid.
errURLError = errors.New("url")
Expand Down
4 changes: 3 additions & 1 deletion pkg/attestation/crafter/runners/runners.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package runners
import (
"fmt"
"os"

"github.com/chainloop-dev/chainloop/pkg/attestation/crafter/runners/oidc"
)

type EnvVarDefinition struct {
Expand Down Expand Up @@ -62,7 +64,7 @@ func (r RunnerEnvironment) String() string {
case Managed:
return "managed"
case SelfHosted:
return "self-hosted"
return oidc.SelfHostedRunner
}
return "unknown"
}
Loading