Skip to content

Commit e6c5853

Browse files
authored
Support Gitlab authentication information (#2008)
Signed-off-by: Rafał Kuć <[email protected]>
1 parent 5c02958 commit e6c5853

File tree

8 files changed

+234
-9
lines changed

8 files changed

+234
-9
lines changed

pkg/attestation/crafter/runner.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ var RunnerFactories = map[schemaapi.CraftingSchema_Runner_RunnerType]RunnerFacto
6767
schemaapi.CraftingSchema_Runner_GITHUB_ACTION: func(logger *zerolog.Logger) SupportedRunner {
6868
return runners.NewGithubAction(timeoutCtx, logger)
6969
},
70-
schemaapi.CraftingSchema_Runner_GITLAB_PIPELINE: func(_ *zerolog.Logger) SupportedRunner {
71-
return runners.NewGitlabPipeline()
70+
schemaapi.CraftingSchema_Runner_GITLAB_PIPELINE: func(logger *zerolog.Logger) SupportedRunner {
71+
return runners.NewGitlabPipeline(timeoutCtx, logger)
7272
},
7373
schemaapi.CraftingSchema_Runner_AZURE_PIPELINE: func(_ *zerolog.Logger) SupportedRunner {
7474
return runners.NewAzurePipeline()

pkg/attestation/crafter/runners/githubaction.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ func (r *GitHubAction) Environment() RunnerEnvironment {
110110
switch r.githubToken.RunnerEnvironment {
111111
case "github-hosted":
112112
return Managed
113-
case "self-hosted":
113+
case oidc.SelfHostedRunner:
114114
return SelfHosted
115115
default:
116116
return Unknown

pkg/attestation/crafter/runners/gitlabpipeline.go

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,30 @@
1616
package runners
1717

1818
import (
19+
"context"
1920
"os"
2021

2122
schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
23+
"github.com/chainloop-dev/chainloop/pkg/attestation/crafter/runners/oidc"
24+
"github.com/rs/zerolog"
2225
)
2326

24-
type GitlabPipeline struct{}
27+
type GitlabPipeline struct {
28+
gitlabToken *oidc.GitlabToken
29+
}
30+
31+
func NewGitlabPipeline(ctx context.Context, logger *zerolog.Logger) *GitlabPipeline {
32+
client, err := oidc.NewGitlabClient(ctx, logger)
33+
if err != nil {
34+
logger.Debug().Err(err).Msgf("failed to create Gitlab OIDC client: %v", err)
35+
return &GitlabPipeline{
36+
gitlabToken: nil,
37+
}
38+
}
2539

26-
func NewGitlabPipeline() *GitlabPipeline {
27-
return &GitlabPipeline{}
40+
return &GitlabPipeline{
41+
gitlabToken: client.Token,
42+
}
2843
}
2944

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

6782
func (r *GitlabPipeline) WorkflowFilePath() string {
83+
if r.gitlabToken != nil {
84+
return r.gitlabToken.ConfigRefURI
85+
}
6886
return ""
6987
}
7088

7189
func (r *GitlabPipeline) IsAuthenticated() bool {
72-
return false
90+
return r.gitlabToken != nil
7391
}
7492

7593
func (r *GitlabPipeline) Environment() RunnerEnvironment {
94+
if r.gitlabToken != nil {
95+
switch r.gitlabToken.RunnerEnvironment {
96+
case "gitlab-hosted":
97+
return Managed
98+
case oidc.SelfHostedRunner:
99+
return SelfHosted
100+
default:
101+
return Unknown
102+
}
103+
}
76104
return Unknown
77105
}

pkg/attestation/crafter/runners/gitlabpipeline_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616
package runners
1717

1818
import (
19+
"context"
1920
"os"
2021
"testing"
2122

23+
"github.com/rs/zerolog"
2224
"github.com/stretchr/testify/assert"
2325
"github.com/stretchr/testify/suite"
2426
)
@@ -117,7 +119,8 @@ func (s *gitlabPipelineSuite) TestRunnerName() {
117119

118120
// Run before each test
119121
func (s *gitlabPipelineSuite) SetupTest() {
120-
s.runner = NewGitlabPipeline()
122+
logger := zerolog.New(zerolog.Nop()).Level(zerolog.Disabled)
123+
s.runner = NewGitlabPipeline(context.Background(), &logger)
121124
t := s.T()
122125
t.Setenv("GITLAB_CI", "true")
123126
t.Setenv("GITLAB_USER_EMAIL", "[email protected]")
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
//
2+
// Copyright 2025 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package oidc
17+
18+
import (
19+
"context"
20+
"fmt"
21+
"os"
22+
23+
"github.com/coreos/go-oidc/v3/oidc"
24+
"github.com/rs/zerolog"
25+
)
26+
27+
// GitlabTokenEnv is the environment variable name for Gitlab OIDC token.
28+
// #nosec G101 - This is just the name of an environment variable, not a credential
29+
const GitlabTokenEnv = "GITLAB_OIDC"
30+
31+
// CIServerURLEnv is the environment variable name for Gitlab CI server URL.
32+
const CIServerURLEnv = "CI_SERVER_URL"
33+
34+
type GitlabToken struct {
35+
oidc.IDToken
36+
37+
// ConfigRefURI is a reference to the current job workflow.
38+
ConfigRefURI string `json:"ci_config_ref_uri"`
39+
40+
// RunnerEnvironment is the environment the runner is running in.
41+
RunnerEnvironment string `json:"runner_environment"`
42+
}
43+
44+
type GitlabOIDCClient struct {
45+
Token *GitlabToken
46+
}
47+
48+
func NewGitlabClient(ctx context.Context, logger *zerolog.Logger) (*GitlabOIDCClient, error) {
49+
var c GitlabOIDCClient
50+
51+
// retrieve the Gitlab server on which the pipeline is running, which is the provider URL
52+
providerURL := os.Getenv(CIServerURLEnv)
53+
logger.Debug().Str("providerURL", providerURL).Msg("retrieved provider URL")
54+
if providerURL == "" {
55+
return nil, fmt.Errorf("%s environment variable not set", CIServerURLEnv)
56+
}
57+
58+
tokenContent := os.Getenv(GitlabTokenEnv)
59+
logger.Debug().Msg("retrieved token content")
60+
if tokenContent == "" {
61+
return nil, fmt.Errorf("%s environment variable not set", GitlabTokenEnv)
62+
}
63+
64+
token, err := parseToken(ctx, providerURL, tokenContent)
65+
if err != nil {
66+
return nil, fmt.Errorf("failed to parse token: %w", err)
67+
}
68+
69+
c.Token = token
70+
return &c, nil
71+
}
72+
73+
func parseToken(ctx context.Context, providerURL string, tokenString string) (*GitlabToken, error) {
74+
provider, err := oidc.NewProvider(ctx, providerURL)
75+
if err != nil {
76+
return nil, fmt.Errorf("failed to connect to OIDC provider: %w", err)
77+
}
78+
79+
verifier := provider.Verifier(&oidc.Config{
80+
SkipClientIDCheck: true, // Skip client ID check since we're just parsing
81+
})
82+
83+
idToken, err := verifier.Verify(ctx, tokenString)
84+
if err != nil {
85+
return nil, fmt.Errorf("token verification failed: %w", err)
86+
}
87+
88+
token := &GitlabToken{
89+
IDToken: *idToken,
90+
}
91+
92+
// Extract claims to populate our custom fields
93+
var claims map[string]interface{}
94+
if err := idToken.Claims(&claims); err != nil {
95+
return nil, fmt.Errorf("failed to extract claims: %w", err)
96+
}
97+
98+
if configRefURI, ok := claims["ci_config_ref_uri"].(string); ok {
99+
token.ConfigRefURI = configRefURI
100+
}
101+
102+
if runnerEnv, ok := claims["runner_environment"].(string); ok {
103+
token.RunnerEnvironment = runnerEnv
104+
}
105+
106+
return token, nil
107+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//
2+
// Copyright 2025 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package oidc_test
17+
18+
import (
19+
"context"
20+
"os"
21+
"testing"
22+
23+
"github.com/chainloop-dev/chainloop/pkg/attestation/crafter/runners/oidc"
24+
"github.com/rs/zerolog"
25+
"github.com/stretchr/testify/assert"
26+
)
27+
28+
func TestNewGitlabClient(t *testing.T) {
29+
testLogger := zerolog.New(zerolog.Nop()).Level(zerolog.Disabled)
30+
ctx := context.Background()
31+
32+
// Save original environment variables
33+
originalServerURL := os.Getenv(oidc.CIServerURLEnv)
34+
originalToken := os.Getenv(oidc.GitlabTokenEnv)
35+
defer func() {
36+
t.Setenv(oidc.CIServerURLEnv, originalServerURL)
37+
t.Setenv(oidc.GitlabTokenEnv, originalToken)
38+
}()
39+
40+
tests := []struct {
41+
name string
42+
setupEnv func(t *testing.T)
43+
expectErr bool
44+
expectErrContains string
45+
}{
46+
{
47+
name: "Missing server URL",
48+
setupEnv: func(t *testing.T) {
49+
t.Setenv(oidc.CIServerURLEnv, "")
50+
t.Setenv(oidc.GitlabTokenEnv, "test-token")
51+
},
52+
expectErr: true,
53+
expectErrContains: "environment variable not set",
54+
},
55+
{
56+
name: "Missing OIDC token",
57+
setupEnv: func(t *testing.T) {
58+
t.Setenv(oidc.CIServerURLEnv, "https://gitlab.example.com")
59+
t.Setenv(oidc.GitlabTokenEnv, "")
60+
},
61+
expectErr: true,
62+
expectErrContains: "environment variable not set",
63+
},
64+
}
65+
66+
for _, tt := range tests {
67+
t.Run(tt.name, func(t *testing.T) {
68+
tt.setupEnv(t)
69+
client, err := oidc.NewGitlabClient(ctx, &testLogger)
70+
71+
if tt.expectErr {
72+
assert.Error(t, err)
73+
if tt.expectErrContains != "" {
74+
assert.Contains(t, err.Error(), tt.expectErrContains)
75+
}
76+
assert.Nil(t, client)
77+
} else {
78+
assert.NoError(t, err)
79+
assert.NotNil(t, client)
80+
}
81+
})
82+
}
83+
}

pkg/attestation/crafter/runners/oidc/oidc.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020
"errors"
2121
)
2222

23+
const SelfHostedRunner = "self-hosted"
24+
2325
var (
2426
// errURLError indicates the OIDC server URL is invalid.
2527
errURLError = errors.New("url")

pkg/attestation/crafter/runners/runners.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ package runners
1818
import (
1919
"fmt"
2020
"os"
21+
22+
"github.com/chainloop-dev/chainloop/pkg/attestation/crafter/runners/oidc"
2123
)
2224

2325
type EnvVarDefinition struct {
@@ -62,7 +64,7 @@ func (r RunnerEnvironment) String() string {
6264
case Managed:
6365
return "managed"
6466
case SelfHosted:
65-
return "self-hosted"
67+
return oidc.SelfHostedRunner
6668
}
6769
return "unknown"
6870
}

0 commit comments

Comments
 (0)