From 67743684840924ccb1d5e2f6699f4e1230b5fe67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ku=C4=87?= Date: Wed, 30 Apr 2025 14:26:58 +0200 Subject: [PATCH 1/3] Next set of changes related to Gitlab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Kuć --- pkg/attestation/crafter/runner.go | 2 +- .../crafter/runners/gitlabpipeline.go | 35 +++++- .../crafter/runners/gitlabpipeline_test.go | 3 +- .../crafter/runners/oidc/gitlab.go | 104 ++++++++++++++++++ 4 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 pkg/attestation/crafter/runners/oidc/gitlab.go diff --git a/pkg/attestation/crafter/runner.go b/pkg/attestation/crafter/runner.go index a586a3d4b..f61639cfd 100644 --- a/pkg/attestation/crafter/runner.go +++ b/pkg/attestation/crafter/runner.go @@ -61,7 +61,7 @@ var timeoutCtx, _ = context.WithTimeout(context.Background(), 15*time.Second) var RunnersMap = map[schemaapi.CraftingSchema_Runner_RunnerType]SupportedRunner{ schemaapi.CraftingSchema_Runner_GITHUB_ACTION: runners.NewGithubAction(timeoutCtx), - schemaapi.CraftingSchema_Runner_GITLAB_PIPELINE: runners.NewGitlabPipeline(), + schemaapi.CraftingSchema_Runner_GITLAB_PIPELINE: runners.NewGitlabPipeline(timeoutCtx), schemaapi.CraftingSchema_Runner_AZURE_PIPELINE: runners.NewAzurePipeline(), schemaapi.CraftingSchema_Runner_JENKINS_JOB: runners.NewJenkinsJob(), schemaapi.CraftingSchema_Runner_CIRCLECI_BUILD: runners.NewCircleCIBuild(), diff --git a/pkg/attestation/crafter/runners/gitlabpipeline.go b/pkg/attestation/crafter/runners/gitlabpipeline.go index e537b4ec4..79b94f6ea 100644 --- a/pkg/attestation/crafter/runners/gitlabpipeline.go +++ b/pkg/attestation/crafter/runners/gitlabpipeline.go @@ -16,15 +16,29 @@ 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" ) -type GitlabPipeline struct{} +type GitlabPipeline struct { + gitlabToken *oidc.GitlabToken +} + +func NewGitlabPipeline(ctx context.Context) *GitlabPipeline { + client, err := oidc.NewGitlabClient(ctx) + if err != nil { + // TODO: add logging + return &GitlabPipeline{ + gitlabToken: nil, + } + } -func NewGitlabPipeline() *GitlabPipeline { - return &GitlabPipeline{} + return &GitlabPipeline{ + gitlabToken: client.Token, + } } func (r *GitlabPipeline) ID() schemaapi.CraftingSchema_Runner_RunnerType { @@ -65,13 +79,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 "self-hosted": + return SelfHosted + default: + return Unknown + } + } return Unknown } diff --git a/pkg/attestation/crafter/runners/gitlabpipeline_test.go b/pkg/attestation/crafter/runners/gitlabpipeline_test.go index a339b4824..0b015bee7 100644 --- a/pkg/attestation/crafter/runners/gitlabpipeline_test.go +++ b/pkg/attestation/crafter/runners/gitlabpipeline_test.go @@ -16,6 +16,7 @@ package runners import ( + "context" "os" "testing" @@ -117,7 +118,7 @@ func (s *gitlabPipelineSuite) TestRunnerName() { // Run before each test func (s *gitlabPipelineSuite) SetupTest() { - s.runner = NewGitlabPipeline() + s.runner = NewGitlabPipeline(context.Background()) t := s.T() t.Setenv("GITLAB_CI", "true") t.Setenv("GITLAB_USER_EMAIL", "foo@foo.com") diff --git a/pkg/attestation/crafter/runners/oidc/gitlab.go b/pkg/attestation/crafter/runners/oidc/gitlab.go new file mode 100644 index 000000000..ecaeeb999 --- /dev/null +++ b/pkg/attestation/crafter/runners/oidc/gitlab.go @@ -0,0 +1,104 @@ +// +// 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" +) + +// GITLAB_OIDC_TOKEN_ENV_KEY is the environment variable name for Gitlab OIDC token. +var GITLAB_OIDC_TOKEN_ENV_KEY = "GITLAB_OIDC" + +// CI_SERVER_URL_ENV_KEY is the environment variable name for Gitlab CI server URL. +var CI_SERVER_URL_ENV_KEY = "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) (*GitlabOIDCClient, error) { + var c GitlabOIDCClient + + // retrieve the Gitlab server on which the pipeline is running, which is the provider URL + providerURL := os.Getenv(CI_SERVER_URL_ENV_KEY) + if providerURL == "" { + return nil, fmt.Errorf("%s environment variable not set", CI_SERVER_URL_ENV_KEY) + } + + tokenContent := os.Getenv(GITLAB_OIDC_TOKEN_ENV_KEY) + if tokenContent == "" { + return nil, fmt.Errorf("%s environment variable not set", GITLAB_OIDC_TOKEN_ENV_KEY) + } + + 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: %v", err) + } + + verifier := provider.Verifier(&oidc.Config{ + SkipClientIDCheck: true, // Skip client ID check since we're just parsing + SkipExpiryCheck: true, // Skip expiry check to allow viewing expired tokens + }) + + idToken, err := verifier.Verify(ctx, tokenString) + if err != nil { + return nil, fmt.Errorf("token verification failed: %v", 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: %v", 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 +} From 3d041b8f7fbb8b45dfa014f05ef1c13a0ac6536a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ku=C4=87?= Date: Wed, 30 Apr 2025 17:05:34 +0200 Subject: [PATCH 2/3] Lint errors fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Kuć --- .../crafter/runners/githubaction.go | 2 +- .../crafter/runners/gitlabpipeline.go | 2 +- .../crafter/runners/oidc/gitlab.go | 26 ++++---- .../crafter/runners/oidc/gitlab_test.go | 65 +++---------------- pkg/attestation/crafter/runners/oidc/oidc.go | 2 + pkg/attestation/crafter/runners/runners.go | 4 +- 6 files changed, 28 insertions(+), 73 deletions(-) diff --git a/pkg/attestation/crafter/runners/githubaction.go b/pkg/attestation/crafter/runners/githubaction.go index 4a33f6a1a..ba3976c7f 100644 --- a/pkg/attestation/crafter/runners/githubaction.go +++ b/pkg/attestation/crafter/runners/githubaction.go @@ -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 diff --git a/pkg/attestation/crafter/runners/gitlabpipeline.go b/pkg/attestation/crafter/runners/gitlabpipeline.go index 4c9e86597..170e04e04 100644 --- a/pkg/attestation/crafter/runners/gitlabpipeline.go +++ b/pkg/attestation/crafter/runners/gitlabpipeline.go @@ -95,7 +95,7 @@ func (r *GitlabPipeline) Environment() RunnerEnvironment { switch r.gitlabToken.RunnerEnvironment { case "gitlab-hosted": return Managed - case "self-hosted": + case oidc.SelfHostedRunner: return SelfHosted default: return Unknown diff --git a/pkg/attestation/crafter/runners/oidc/gitlab.go b/pkg/attestation/crafter/runners/oidc/gitlab.go index 6f0ae8742..f561d641f 100644 --- a/pkg/attestation/crafter/runners/oidc/gitlab.go +++ b/pkg/attestation/crafter/runners/oidc/gitlab.go @@ -24,11 +24,12 @@ import ( "github.com/rs/zerolog" ) -// GITLAB_OIDC_TOKEN_ENV_KEY is the environment variable name for Gitlab OIDC token. -var GITLAB_OIDC_TOKEN_ENV_KEY = "GITLAB_OIDC" +// 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" -// CI_SERVER_URL_ENV_KEY is the environment variable name for Gitlab CI server URL. -var CI_SERVER_URL_ENV_KEY = "CI_SERVER_URL" +// CIServerURLEnv is the environment variable name for Gitlab CI server URL. +const CIServerURLEnv = "CI_SERVER_URL" type GitlabToken struct { oidc.IDToken @@ -41,24 +42,23 @@ type GitlabToken struct { } type GitlabOIDCClient struct { - logger *zerolog.Logger - Token *GitlabToken + 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(CI_SERVER_URL_ENV_KEY) + providerURL := os.Getenv(CIServerURLEnv) logger.Debug().Str("providerURL", providerURL).Msg("retrieved provider URL") if providerURL == "" { - return nil, fmt.Errorf("%s environment variable not set", CI_SERVER_URL_ENV_KEY) + return nil, fmt.Errorf("%s environment variable not set", CIServerURLEnv) } - tokenContent := os.Getenv(GITLAB_OIDC_TOKEN_ENV_KEY) + tokenContent := os.Getenv(GitlabTokenEnv) logger.Debug().Str("tokenContent", tokenContent).Msg("retrieved token content") if tokenContent == "" { - return nil, fmt.Errorf("%s environment variable not set", GITLAB_OIDC_TOKEN_ENV_KEY) + return nil, fmt.Errorf("%s environment variable not set", GitlabTokenEnv) } token, err := parseToken(ctx, providerURL, tokenContent) @@ -73,7 +73,7 @@ func NewGitlabClient(ctx context.Context, logger *zerolog.Logger) (*GitlabOIDCCl 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: %v", err) + return nil, fmt.Errorf("failed to connect to OIDC provider: %w", err) } verifier := provider.Verifier(&oidc.Config{ @@ -83,7 +83,7 @@ func parseToken(ctx context.Context, providerURL string, tokenString string) (*G idToken, err := verifier.Verify(ctx, tokenString) if err != nil { - return nil, fmt.Errorf("token verification failed: %v", err) + return nil, fmt.Errorf("token verification failed: %w", err) } token := &GitlabToken{ @@ -93,7 +93,7 @@ func parseToken(ctx context.Context, providerURL string, tokenString string) (*G // 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: %v", err) + return nil, fmt.Errorf("failed to extract claims: %w", err) } if configRefURI, ok := claims["ci_config_ref_uri"].(string); ok { diff --git a/pkg/attestation/crafter/runners/oidc/gitlab_test.go b/pkg/attestation/crafter/runners/oidc/gitlab_test.go index 2ea093f33..934c4b322 100644 --- a/pkg/attestation/crafter/runners/oidc/gitlab_test.go +++ b/pkg/attestation/crafter/runners/oidc/gitlab_test.go @@ -17,9 +17,6 @@ package oidc_test import ( "context" - "encoding/json" - "net/http" - "net/http/httptest" "os" "testing" @@ -33,11 +30,11 @@ func TestNewGitlabClient(t *testing.T) { ctx := context.Background() // Save original environment variables - originalServerURL := os.Getenv(oidc.CI_SERVER_URL_ENV_KEY) - originalToken := os.Getenv(oidc.GITLAB_OIDC_TOKEN_ENV_KEY) + originalServerURL := os.Getenv(oidc.CIServerURLEnv) + originalToken := os.Getenv(oidc.GitlabTokenEnv) defer func() { - t.Setenv(oidc.CI_SERVER_URL_ENV_KEY, originalServerURL) - t.Setenv(oidc.GITLAB_OIDC_TOKEN_ENV_KEY, originalToken) + t.Setenv(oidc.CIServerURLEnv, originalServerURL) + t.Setenv(oidc.GitlabTokenEnv, originalToken) }() tests := []struct { @@ -49,8 +46,8 @@ func TestNewGitlabClient(t *testing.T) { { name: "Missing server URL", setupEnv: func(t *testing.T) { - t.Setenv(oidc.CI_SERVER_URL_ENV_KEY, "") - t.Setenv(oidc.GITLAB_OIDC_TOKEN_ENV_KEY, "test-token") + t.Setenv(oidc.CIServerURLEnv, "") + t.Setenv(oidc.GitlabTokenEnv, "test-token") }, expectErr: true, expectErrContains: "environment variable not set", @@ -58,14 +55,12 @@ func TestNewGitlabClient(t *testing.T) { { name: "Missing OIDC token", setupEnv: func(t *testing.T) { - t.Setenv(oidc.CI_SERVER_URL_ENV_KEY, "https://gitlab.example.com") - t.Setenv(oidc.GITLAB_OIDC_TOKEN_ENV_KEY, "") + t.Setenv(oidc.CIServerURLEnv, "https://gitlab.example.com") + t.Setenv(oidc.GitlabTokenEnv, "") }, expectErr: true, expectErrContains: "environment variable not set", }, - // We can't easily test the successful case without mocking the OIDC provider - // which would require significant refactoring or a mocking library } for _, tt := range tests { @@ -86,47 +81,3 @@ func TestNewGitlabClient(t *testing.T) { }) } } - -// This test requires mocking the OIDC provider, which is challenging -// without refactoring the code for better testability. -// Here's a sketch of how such a test might look: -func TestParseTokenWithMockProvider(t *testing.T) { - t.Skip("This test requires mocking the OIDC provider, which is not implemented yet") - - // Setup a mock OIDC provider server - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case "/.well-known/openid-configuration": - // Respond with a mock OIDC configuration - json.NewEncoder(w).Encode(map[string]interface{}{ - "issuer": "https://mock-gitlab.example.com", - "jwks_uri": "https://mock-gitlab.example.com/jwks", - "token_endpoint": "https://mock-gitlab.example.com/token", - "userinfo_endpoint": "https://mock-gitlab.example.com/userinfo", - "authorization_endpoint": "https://mock-gitlab.example.com/authorize", - }) - case "/jwks": - // Respond with mock JWKs - // This would need to include the public keys corresponding to the private keys - // used to sign the test tokens - json.NewEncoder(w).Encode(map[string]interface{}{ - "keys": []map[string]interface{}{ - // Mock key data would go here - }, - }) - default: - w.WriteHeader(http.StatusNotFound) - } - })) - defer server.Close() - - // For a real test, we would need to: - // 1. Create a valid JWT token signed with a private key - // 2. Configure the mock server to return the corresponding public key - // 3. Call the parseToken function with the mock server URL and token - // 4. Verify the token and claims are correctly extracted - - // This would require either: - // - Making parseToken public or - // - Refactoring the code to accept a provider interface that can be mocked -} diff --git a/pkg/attestation/crafter/runners/oidc/oidc.go b/pkg/attestation/crafter/runners/oidc/oidc.go index 77e71758b..a3510a8fb 100644 --- a/pkg/attestation/crafter/runners/oidc/oidc.go +++ b/pkg/attestation/crafter/runners/oidc/oidc.go @@ -20,6 +20,8 @@ import ( "errors" ) +const SelfHostedRunner = "self-hosted" + var ( // errURLError indicates the OIDC server URL is invalid. errURLError = errors.New("url") diff --git a/pkg/attestation/crafter/runners/runners.go b/pkg/attestation/crafter/runners/runners.go index 9ba43c802..c7834a487 100644 --- a/pkg/attestation/crafter/runners/runners.go +++ b/pkg/attestation/crafter/runners/runners.go @@ -18,6 +18,8 @@ package runners import ( "fmt" "os" + + "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/runners/oidc" ) type EnvVarDefinition struct { @@ -62,7 +64,7 @@ func (r RunnerEnvironment) String() string { case Managed: return "managed" case SelfHosted: - return "self-hosted" + return oidc.SelfHostedRunner } return "unknown" } From b23c94376670fced51aa4a9ebc5ff72446f13d5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Ku=C4=87?= Date: Fri, 2 May 2025 13:44:50 +0200 Subject: [PATCH 3/3] Adjustments after PR review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Kuć --- pkg/attestation/crafter/runners/gitlabpipeline.go | 2 +- pkg/attestation/crafter/runners/oidc/gitlab.go | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/attestation/crafter/runners/gitlabpipeline.go b/pkg/attestation/crafter/runners/gitlabpipeline.go index 170e04e04..af3543864 100644 --- a/pkg/attestation/crafter/runners/gitlabpipeline.go +++ b/pkg/attestation/crafter/runners/gitlabpipeline.go @@ -31,7 +31,7 @@ type GitlabPipeline struct { func NewGitlabPipeline(ctx context.Context, logger *zerolog.Logger) *GitlabPipeline { client, err := oidc.NewGitlabClient(ctx, logger) if err != nil { - logger.Debug().Err(err).Msg("failed to create Gitlab OIDC client") + logger.Debug().Err(err).Msgf("failed to create Gitlab OIDC client: %v", err) return &GitlabPipeline{ gitlabToken: nil, } diff --git a/pkg/attestation/crafter/runners/oidc/gitlab.go b/pkg/attestation/crafter/runners/oidc/gitlab.go index f561d641f..d29668ec6 100644 --- a/pkg/attestation/crafter/runners/oidc/gitlab.go +++ b/pkg/attestation/crafter/runners/oidc/gitlab.go @@ -56,7 +56,7 @@ func NewGitlabClient(ctx context.Context, logger *zerolog.Logger) (*GitlabOIDCCl } tokenContent := os.Getenv(GitlabTokenEnv) - logger.Debug().Str("tokenContent", tokenContent).Msg("retrieved token content") + logger.Debug().Msg("retrieved token content") if tokenContent == "" { return nil, fmt.Errorf("%s environment variable not set", GitlabTokenEnv) } @@ -78,7 +78,6 @@ func parseToken(ctx context.Context, providerURL string, tokenString string) (*G verifier := provider.Verifier(&oidc.Config{ SkipClientIDCheck: true, // Skip client ID check since we're just parsing - SkipExpiryCheck: true, // Skip expiry check to allow viewing expired tokens }) idToken, err := verifier.Verify(ctx, tokenString)