-
Notifications
You must be signed in to change notification settings - Fork 37
feat: add GitLab CI OIDC detector #302
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
48 changes: 48 additions & 0 deletions
48
cloudsmith_cli/core/credentials/oidc/detectors/gitlab_ci.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| # Copyright 2026 Cloudsmith Ltd | ||
| """GitLab CI OIDC detector. | ||
|
|
||
| Reads an OIDC token from environment variables populated by GitLab's | ||
| ``id_tokens`` configuration in ``.gitlab-ci.yml``. | ||
|
|
||
| References: | ||
| https://docs.gitlab.com/ci/cloud_services/ | ||
| https://docs.cloudsmith.com/integrations/integrating-with-gitlab-cicd | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import os | ||
|
|
||
| from .base import EnvironmentDetector | ||
|
|
||
|
|
||
| class GitLabCIDetector(EnvironmentDetector): | ||
| """Detects GitLab CI and reads an OIDC token from an environment variable. | ||
|
|
||
| GitLab requires users to configure ``id_tokens`` in ``.gitlab-ci.yml``, | ||
| minting a token with ``aud`` set to the Cloudsmith OIDC endpoint and | ||
| exposing it as ``CLOUDSMITH_OIDC_TOKEN``. The legacy ``CI_JOB_JWT``/ | ||
| ``CI_JOB_JWT_V2`` variables are deliberately not consulted: they were | ||
| removed in GitLab 17.0, carry the GitLab instance URL as their audience | ||
| (not the Cloudsmith audience the token exchange validates), and were | ||
| auto-injected into every job on older instances. | ||
| """ | ||
|
|
||
| name = "GitLab CI" | ||
|
|
||
| TOKEN_ENV_VAR = "CLOUDSMITH_OIDC_TOKEN" | ||
|
|
||
| def detect(self) -> bool: | ||
| if os.environ.get("GITLAB_CI") != "true": | ||
| return False | ||
| return bool(os.environ.get(self.TOKEN_ENV_VAR)) | ||
|
|
||
| def get_token(self) -> str: | ||
| token = os.environ.get(self.TOKEN_ENV_VAR) | ||
| if token: | ||
| return token | ||
| raise ValueError( | ||
| "GitLab CI detected but no OIDC token found. " | ||
| "Configure id_tokens in .gitlab-ci.yml and expose it as " | ||
| + self.TOKEN_ENV_VAR | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| """Tests for the GitLab CI OIDC detector.""" | ||
|
|
||
| from unittest import mock | ||
|
|
||
| import pytest | ||
|
|
||
| from cloudsmith_cli.core.credentials.models import CredentialContext | ||
| from cloudsmith_cli.core.credentials.oidc.detectors import detect_environment | ||
| from cloudsmith_cli.core.credentials.oidc.detectors.gitlab_ci import GitLabCIDetector | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def gitlab_env(): | ||
| env = { | ||
| "GITLAB_CI": "true", | ||
| "CLOUDSMITH_OIDC_TOKEN": "the-jwt", | ||
| } | ||
| with mock.patch.dict("os.environ", env, clear=True): | ||
| yield env | ||
|
|
||
|
|
||
| class TestDetect: | ||
| def test_detects_when_gitlab_ci_and_token_present(self, gitlab_env): | ||
| detector = GitLabCIDetector(context=CredentialContext()) | ||
| assert detector.detect() is True | ||
|
|
||
| def test_not_detected_when_unset(self): | ||
| with mock.patch.dict("os.environ", {}, clear=True): | ||
| detector = GitLabCIDetector(context=CredentialContext()) | ||
| assert detector.detect() is False | ||
|
|
||
| def test_not_detected_without_gitlab_ci_flag(self, gitlab_env): | ||
| del gitlab_env["GITLAB_CI"] | ||
| with mock.patch.dict("os.environ", gitlab_env, clear=True): | ||
| detector = GitLabCIDetector(context=CredentialContext()) | ||
| assert detector.detect() is False | ||
|
|
||
| def test_not_detected_when_gitlab_ci_not_true(self, gitlab_env): | ||
| gitlab_env["GITLAB_CI"] = "false" | ||
| with mock.patch.dict("os.environ", gitlab_env, clear=True): | ||
| detector = GitLabCIDetector(context=CredentialContext()) | ||
| assert detector.detect() is False | ||
|
|
||
| def test_not_detected_without_any_token(self, gitlab_env): | ||
| del gitlab_env["CLOUDSMITH_OIDC_TOKEN"] | ||
| with mock.patch.dict("os.environ", gitlab_env, clear=True): | ||
| detector = GitLabCIDetector(context=CredentialContext()) | ||
| assert detector.detect() is False | ||
|
|
||
| def test_not_detected_with_legacy_ci_job_jwt(self, gitlab_env): | ||
| del gitlab_env["CLOUDSMITH_OIDC_TOKEN"] | ||
| gitlab_env["CI_JOB_JWT_V2"] = "legacy-jwt" | ||
| with mock.patch.dict("os.environ", gitlab_env, clear=True): | ||
| detector = GitLabCIDetector(context=CredentialContext()) | ||
| assert detector.detect() is False | ||
|
|
||
|
|
||
| class TestGetToken: | ||
| def test_returns_token(self, gitlab_env): | ||
| detector = GitLabCIDetector(context=CredentialContext()) | ||
| assert detector.get_token() == "the-jwt" | ||
|
|
||
| def test_raises_when_no_token(self, gitlab_env): | ||
| del gitlab_env["CLOUDSMITH_OIDC_TOKEN"] | ||
| with mock.patch.dict("os.environ", gitlab_env, clear=True): | ||
| detector = GitLabCIDetector(context=CredentialContext()) | ||
| with pytest.raises(ValueError): | ||
| detector.get_token() | ||
|
|
||
|
|
||
| class TestIntegration: | ||
| def test_detect_environment_selects_gitlab_ci(self, gitlab_env): | ||
| detector = detect_environment(CredentialContext()) | ||
| assert isinstance(detector, GitLabCIDetector) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.