Skip to content
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

Support gitlab #123

Merged
merged 7 commits into from
Oct 22, 2023
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: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]

### Added

* Added support for GitLab CI/CD ([#123](https://github.com/di/id/pull/123))

## [1.1.0]

### Added
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ the OIDC token. This should be set to the intended audience for the token.
* [Compute Engine](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances)
* and more
* [Buildkite](https://buildkite.com/docs/agent/v3/cli-oidc)
* [GitLab](https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html) (See _environment variables_ below)

### Tokens in environment variables

GitLab provides OIDC tokens through environment variables. The variable name must be
`<AUD>_ID_TOKEN` where `<AUD>` is the uppercased audience argument where all
characters outside of ASCII letters and digits are replaced with "_". A leading digit
must also be replaced with a "_".

## Licensing

Expand Down
2 changes: 2 additions & 0 deletions id/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,14 @@ def detect_credential(audience: str) -> Optional[str]:
detect_buildkite,
detect_gcp,
detect_github,
detect_gitlab,
)

detectors: List[Callable[..., Optional[str]]] = [
detect_github,
detect_gcp,
detect_buildkite,
detect_gitlab,
]
for detector in detectors:
credential = detector(audience)
Expand Down
40 changes: 40 additions & 0 deletions id/_internal/oidc/ambient.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import logging
import os
import re
import shutil
import subprocess # nosec B404
from typing import Optional
Expand All @@ -34,6 +35,8 @@
_GCP_IDENTITY_REQUEST_URL = "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity" # noqa
_GCP_GENERATEIDTOKEN_REQUEST_URL = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{}:generateIdToken" # noqa

_env_var_regex = re.compile(r"[^A-Z0-9_]|^[^A-Z_]")


class _GitHubTokenPayload(BaseModel):
"""
Expand Down Expand Up @@ -258,3 +261,40 @@ def detect_buildkite(audience: str) -> Optional[str]:
)

return process.stdout.strip()


def detect_gitlab(audience: str) -> Optional[str]:
"""
Detect and return a GitLab CI/CD ambient OIDC credential.

This detection is based on an environment variable. The variable name must be
`<AUD>_ID_TOKEN` where `<AUD>` is the uppercased audience argument where all
characters outside of ASCII letters and digits are replaced with "_". A
leading digit must also replaced with a "_".

As an example, audience "sigstore" would require variable SIGSTORE_ID_TOKEN,
and audience "http://test.audience" would require variable
HTTP___TEST_AUDIENCE_ID_TOKEN.

Returns `None` if the context is not GitLab CI/CD environment.

Raises if the environment is GitLab, but the `<AUD>_ID_TOKEN` environment
variable is not set.
"""
logger.debug("GitLab: looking for OIDC credentials")

if not os.getenv("GITLAB_CI"):
logger.debug("GitLab: environment doesn't look like GitLab CI/CD; giving up")
return None

# construct a reasonable env var name from the audience
sanitized_audience = _env_var_regex.sub("_", audience.upper())
var_name = f"{sanitized_audience}_ID_TOKEN"
token = os.getenv(var_name)
if not token:
raise AmbientCredentialError(
f"GitLab: Environment variable {var_name} not found"
)

logger.debug(f"GitLab: Found token in environment variable {var_name}")
return token
65 changes: 65 additions & 0 deletions test/unit/internal/oidc/test_ambient.py
Original file line number Diff line number Diff line change
Expand Up @@ -641,3 +641,68 @@ def test_buildkite(monkeypatch):
text=True,
)
]


def test_buildkite_bad_env(monkeypatch):
monkeypatch.delenv("BUILDKITE", False)

logger = pretend.stub(debug=pretend.call_recorder(lambda s: None))
monkeypatch.setattr(ambient, "logger", logger)

assert ambient.detect_buildkite("some-audience") is None
assert logger.debug.calls == [
pretend.call("Buildkite: looking for OIDC credentials"),
pretend.call("Buildkite: environment doesn't look like BuildKite; giving up"),
]


def test_gitlab_bad_env(monkeypatch):
monkeypatch.delenv("GITLAB_CI", False)

logger = pretend.stub(debug=pretend.call_recorder(lambda s: None))
monkeypatch.setattr(ambient, "logger", logger)

assert ambient.detect_gitlab("some-audience") is None
assert logger.debug.calls == [
pretend.call("GitLab: looking for OIDC credentials"),
pretend.call("GitLab: environment doesn't look like GitLab CI/CD; giving up"),
]


def test_gitlab_no_variable(monkeypatch):
monkeypatch.setenv("GITLAB_CI", "true")

logger = pretend.stub(debug=pretend.call_recorder(lambda s: None))
monkeypatch.setattr(ambient, "logger", logger)

with pytest.raises(
ambient.AmbientCredentialError,
match="GitLab: Environment variable SOME_AUDIENCE_ID_TOKEN not found",
):
ambient.detect_gitlab("some-audience")

assert logger.debug.calls == [
pretend.call("GitLab: looking for OIDC credentials"),
]


def test_gitlab(monkeypatch):
monkeypatch.setenv("GITLAB_CI", "true")
monkeypatch.setenv("SOME_AUDIENCE_ID_TOKEN", "fakejwt")
monkeypatch.setenv("_1_OTHER_AUDIENCE_ID_TOKEN", "fakejwt2")

logger = pretend.stub(debug=pretend.call_recorder(lambda s: None))
monkeypatch.setattr(ambient, "logger", logger)

assert ambient.detect_gitlab("some-audience") == "fakejwt"
assert ambient.detect_gitlab("11 other audience") == "fakejwt2"
assert logger.debug.calls == [
pretend.call("GitLab: looking for OIDC credentials"),
pretend.call(
"GitLab: Found token in environment variable SOME_AUDIENCE_ID_TOKEN"
),
pretend.call("GitLab: looking for OIDC credentials"),
pretend.call(
"GitLab: Found token in environment variable _1_OTHER_AUDIENCE_ID_TOKEN"
),
]