Skip to content

Commit

Permalink
Support gitlab (#123)
Browse files Browse the repository at this point in the history
* Initial GitLab support (via env var)

* test: Test buildkite when not in buildkite env

This brings ambient.py coverage to 100%.

* test: Test detect_env_var()

* lint fixes

* Add a changelog entry for detect_env_var()

* Refactor env var detection to gitlab detection

Based on feedback:
* Instead of generic env var handling, make the detector
  only work on gitlab (based on GITLAB_CI variable)
* Handle audience args that begin with a digit (replace with "_"
  in the env var name)
* Raise if we are in GitLab environment but token is not found
* Tweak README based on these changes

This does seem much better as a misconfigured pipeline (e.g. a missing
id_tokens section) now results in the following with sigstore:

    $ python -m sigstore sign README.md
                  An issue occurred with ambient credential detection.
                  Additional context:
                  GitLab: Environment variable SIGSTORE_ID_TOKEN not found

which seems pretty good to me.

---------

Co-authored-by: Dustin Ingram <[email protected]>
  • Loading branch information
jku and di authored Oct 22, 2023
1 parent e5f5147 commit 7d73ba4
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 0 deletions.
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"
),
]

0 comments on commit 7d73ba4

Please sign in to comment.