Skip to content

Commit

Permalink
id: add ambient detector for CircleCI (#144)
Browse files Browse the repository at this point in the history
* id: add ambient detector for CircleCI

Signed-off-by: William Woodruff <[email protected]>

* lintage

Replace `isort` and `black` with `ruff`.

Signed-off-by: William Woodruff <[email protected]>

* test: round out CircleCI tests

Signed-off-by: William Woodruff <[email protected]>

* ambient: add bandit ignores

Signed-off-by: William Woodruff <[email protected]>

---------

Signed-off-by: William Woodruff <[email protected]>
  • Loading branch information
woodruffw authored Dec 12, 2023
1 parent 9695feb commit b6ff8e5
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 89 deletions.
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

0 comments on commit b6ff8e5

Please sign in to comment.