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

id: add ambient detector for CircleCI #144

Merged
merged 4 commits into from
Dec 12, 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
6 changes: 2 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,7 @@ run: $(VENV)/pyvenv.cfg
.PHONY: lint
lint: $(VENV)/pyvenv.cfg
. $(VENV_BIN)/activate && \
black --check $(ALL_PY_SRCS) && \
isort --check $(ALL_PY_SRCS) && \
ruff format --check $(ALL_PY_SRCS) && \
ruff $(ALL_PY_SRCS) && \
mypy $(PY_MODULE) && \
bandit -c pyproject.toml -r $(PY_MODULE) && \
Expand All @@ -74,8 +73,7 @@ lint: $(VENV)/pyvenv.cfg
reformat: $(VENV)/pyvenv.cfg
. $(VENV_BIN)/activate && \
ruff --fix $(ALL_PY_SRCS) && \
black $(ALL_PY_SRCS) && \
isort $(ALL_PY_SRCS)
ruff format $(ALL_PY_SRCS)

.PHONY: test
test: $(VENV)/pyvenv.cfg
Expand Down
2 changes: 2 additions & 0 deletions id/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ def detect_credential(audience: str) -> Optional[str]:
"""
from ._internal.oidc.ambient import (
detect_buildkite,
detect_circleci,
detect_gcp,
detect_github,
detect_gitlab,
Expand All @@ -69,6 +70,7 @@ def detect_credential(audience: str) -> Optional[str]:
detect_gcp,
detect_buildkite,
detect_gitlab,
detect_circleci,
]
for detector in detectors:
credential = detector(audience)
Expand Down
4 changes: 1 addition & 3 deletions id/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@ def _parser() -> argparse.ArgumentParser:
description="a tool for generating OIDC identities",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
"-V", "--version", action="version", version=f"%(prog)s {__version__}"
)
parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {__version__}")
parser.add_argument(
"-v",
"--verbose",
Expand Down
61 changes: 49 additions & 12 deletions id/_internal/oidc/ambient.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
Ambient OIDC credential detection.
"""

import json
import logging
import os
import re
Expand All @@ -31,9 +32,15 @@
logger = logging.getLogger(__name__)

_GCP_PRODUCT_NAME_FILE = "/sys/class/dmi/id/product_name"
_GCP_TOKEN_REQUEST_URL = "http://metadata/computeMetadata/v1/instance/service-accounts/default/token" # noqa # nosec B105
_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
_GCP_TOKEN_REQUEST_URL = (
"http://metadata/computeMetadata/v1/instance/service-accounts/default/token" # noqa # nosec B105
)
_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_]")

Expand Down Expand Up @@ -178,15 +185,11 @@ def detect_gcp(audience: str) -> Optional[str]:
with open(_GCP_PRODUCT_NAME_FILE) as f:
name = f.read().strip()
except OSError:
logger.debug(
"GCP: environment doesn't have GCP product name file; giving up"
)
logger.debug("GCP: environment doesn't have GCP product name file; giving up")
return None

if name not in {"Google", "Google Compute Engine"}:
logger.debug(
f"GCP: product name file exists, but product name is {name!r}; giving up"
)
logger.debug(f"GCP: product name file exists, but product name is {name!r}; giving up")
return None

logger.debug("GCP: requesting OIDC token")
Expand Down Expand Up @@ -292,9 +295,43 @@ def detect_gitlab(audience: str) -> Optional[str]:
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"
)
raise AmbientCredentialError(f"GitLab: Environment variable {var_name} not found")

logger.debug(f"GitLab: Found token in environment variable {var_name}")
return token


def detect_circleci(audience: str) -> Optional[str]:
"""
Detect and return a CircleCI ambient OIDC credential.

Returns `None` if the context is not a CircleCI environment.

Raises if the environment is GitHub Actions, but is incorrect or
insufficiently permissioned for an OIDC credential.
"""
logger.debug("CircleCI: looking for OIDC credentials")

if not os.getenv("CIRCLECI"):
logger.debug("CircleCI: environment doesn't look like CircleCI; giving up")
return None

# Check that the circleci executable exists in the `PATH`.
if shutil.which("circleci") is None:
raise AmbientCredentialError("CircleCI: could not find `circleci` in the environment")

# See NOTE on `detect_buildkite` for why we silence these warnings.
payload = json.dumps({"aud": audience})
process = subprocess.run( # nosec B603, B607
["circleci", "run", "oidc", "get", "--claims", payload],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)

if process.returncode != 0:
raise AmbientCredentialError(
f"CircleCI: the `circleci` tool encountered an error: {process.stdout}"
)

return process.stdout.strip()
9 changes: 1 addition & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ Source = "https://github.com/di/id"
test = ["pytest", "pytest-cov", "pretend", "coverage[toml]"]
lint = [
"bandit",
"black",
"isort",
"interrogate",
"mypy",
# NOTE(ww): ruff is under active development, so we pin conservatively here
Expand All @@ -45,11 +43,6 @@ lint = [
]
dev = ["build", "bump >= 1.3.2", "id[test,lint]"]

[tool.isort]
multi_line_output = 3
known_first_party = "id"
include_trailing_comma = true

[tool.interrogate]
# don't enforce documentation coverage for packaging, testing, the virtual
# environment, or the CLI (which is documented separately).
Expand Down Expand Up @@ -85,4 +78,4 @@ exclude_dirs = ["./test"]
line-length = 100
# TODO: Enable "UP" here once Pydantic allows us to:
# See: https://github.com/pydantic/pydantic/issues/4146
select = ["E", "F", "W"]
select = ["I", "E", "F", "W"]
Loading