Skip to content

Commit 00e7fc9

Browse files
feat: add OIDC detector enable/order controls
Add two env-var controls for OIDC detector selection: - CLOUDSMITH_OIDC_<DETECTOR>_DISABLED skips a specific detector, mirroring boto3's AWS_EC2_METADATA_DISABLED convention (only the literal "true", case-insensitive, disables). - CLOUDSMITH_OIDC_DETECTOR_ORDER overrides which detectors are considered and the order they are tried in (comma-separated slugs; unlisted/unknown slugs are skipped). When both are set the order list defines the candidate set and sequence, then the disable flags are applied on top, so a disabled detector is always skipped. Each detector gains a stable `slug` attribute and a public `registered_detectors()` accessor is added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4586050 commit 00e7fc9

13 files changed

Lines changed: 215 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1717
- Added GitLab CI to OIDC credential auto-discovery. When running in GitLab CI/CD, the CLI reads the OIDC token from the `CLOUDSMITH_OIDC_TOKEN` environment variable (configured via `id_tokens` in `.gitlab-ci.yml`, with legacy fallbacks to `CI_JOB_JWT_V2` / `CI_JOB_JWT`) and exchanges it for a Cloudsmith access token. Works out of the box with no extra dependencies.
1818
- Added Google Cloud (GCP) support to OIDC credential auto-discovery. Install with the `gcp` extra (`pip install cloudsmith-cli[gcp]`) to authenticate using the ambient Google Cloud identity: the attached service account on GCE/Cloud Run/GKE/Cloud Functions/App Engine/Cloud Build, a `GOOGLE_APPLICATION_CREDENTIALS` service-account key, or a local `gcloud auth application-default login` session. The CLI discovers credentials via the `google-auth` SDK's Application Default Credentials chain, mints a short-lived OIDC ID token, and exchanges it for a Cloudsmith access token. On Cloud Build, where the metadata server exposes no ID-token endpoint, the token is minted via the IAM Credentials API (`generateIdToken`), which requires the build service account to hold `roles/iam.serviceAccountTokenCreator` on itself. Without the extra installed, the detector skips itself with no errors.
1919
- Added a generic fallback to OIDC credential auto-discovery. When no dedicated environment is detected, the CLI reads an OIDC token from the `CLOUDSMITH_OIDC_TOKEN` environment variable (useful for Jenkins or any custom CI/CD) and exchanges it for a Cloudsmith access token. Works out of the box with no extra dependencies.
20+
- Added controls for OIDC detector selection. Set `CLOUDSMITH_OIDC_<DETECTOR>_DISABLED=true` to skip a specific detector (mirroring boto3's `AWS_EC2_METADATA_DISABLED` convention — only `true` disables), or `CLOUDSMITH_OIDC_DETECTOR_ORDER` to a comma-separated list of detector slugs to override which detectors are considered and the order they are tried in. When both are set, disable flags take precedence over the order list. Detector slugs: `aws`, `azure_devops`, `bitbucket`, `circleci`, `gcp`, `generic`, `github`, `gitlab`.
2021

2122
## [1.18.0] - 2026-06-09
2223

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,15 @@ See the [Cloudsmith GitLab CI/CD integration guide](https://docs.cloudsmith.com/
229229

230230
As a fallback for environments without a dedicated detector (for example Jenkins with the [credentials binding plugin](https://plugins.jenkins.io/credentials-binding/), or any custom CI/CD system), set the `CLOUDSMITH_OIDC_TOKEN` environment variable to an OIDC JWT and the CLI will exchange it for a Cloudsmith access token. This detector runs last, so a dedicated environment is always preferred when present. See the [Cloudsmith Jenkins OIDC guide](https://docs.cloudsmith.com/authentication/setup-jenkins-to-authenticate-to-cloudsmith-using-oidc).
231231

232+
#### Controlling OIDC Detector Selection
233+
234+
By default the CLI tries each detector in a fixed priority order and uses the first that matches. Two environment variables let you override this when a detector matches an environment you don't want it to (for example, the AWS detector matching ambient instance credentials):
235+
236+
- **Disable a detector** — set `CLOUDSMITH_OIDC_<DETECTOR>_DISABLED=true` to skip it entirely. This mirrors boto3's `AWS_EC2_METADATA_DISABLED` convention: only the literal value `true` (case-insensitive) disables; anything else leaves the detector enabled. For example, `CLOUDSMITH_OIDC_AWS_DISABLED=true` skips the AWS detector so an explicitly-set `CLOUDSMITH_OIDC_TOKEN` is picked up by the generic detector instead.
237+
- **Reorder evaluation** — set `CLOUDSMITH_OIDC_DETECTOR_ORDER` to a comma-separated list of detector slugs to control both which detectors are considered and the order they are tried in (first match wins). Slugs not listed are skipped; unrecognised slugs are ignored. For example, `CLOUDSMITH_OIDC_DETECTOR_ORDER=generic,aws` tries the generic detector first and considers only those two.
238+
239+
When both are set, the order list defines the candidate set and sequence, then the `*_DISABLED` flags are applied on top — so a disabled detector is always skipped even if it appears in the order list. Detector slugs are: `aws`, `azure_devops`, `bitbucket`, `circleci`, `gcp`, `generic`, `github`, `gitlab`.
240+
232241
## Configuration
233242

234243
There are two configuration files used by the CLI:

cloudsmith_cli/core/credentials/oidc/detectors/__init__.py

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import annotations
44

55
import logging
6+
import os
67
from typing import TYPE_CHECKING
78

89
from .aws import AWSDetector
@@ -31,12 +32,70 @@
3132
GenericDetector,
3233
]
3334

35+
DETECTOR_ORDER_ENV_VAR = "CLOUDSMITH_OIDC_DETECTOR_ORDER"
36+
37+
38+
def registered_detectors() -> list[type[EnvironmentDetector]]:
39+
"""Return the registered OIDC detectors in their default priority order."""
40+
return list(_DETECTORS)
41+
42+
43+
def _disable_env_var(slug: str) -> str:
44+
return f"CLOUDSMITH_OIDC_{slug.upper()}_DISABLED"
45+
46+
47+
def _is_detector_disabled(slug: str) -> bool:
48+
"""Whether a detector is disabled via its CLOUDSMITH_OIDC_<SLUG>_DISABLED var.
49+
50+
Mirrors boto3's ``AWS_EC2_METADATA_DISABLED`` wording: only the literal
51+
string ``true`` (case-insensitive) disables; anything else is off.
52+
"""
53+
return os.environ.get(_disable_env_var(slug), "false").strip().lower() == "true"
54+
55+
56+
def _ordered_detectors() -> list[type[EnvironmentDetector]]:
57+
"""Detectors in evaluation order, honouring CLOUDSMITH_OIDC_DETECTOR_ORDER.
58+
59+
When the order var is unset/empty the default registration order is used.
60+
Otherwise only the listed slugs are considered, in the listed order;
61+
unknown slugs are logged and skipped.
62+
"""
63+
raw_order = os.environ.get(DETECTOR_ORDER_ENV_VAR, "").strip()
64+
if not raw_order:
65+
return list(_DETECTORS)
66+
67+
detectors_by_slug = {detector_cls.slug: detector_cls for detector_cls in _DETECTORS}
68+
ordered: list[type[EnvironmentDetector]] = []
69+
for token in raw_order.split(","):
70+
slug = token.strip().lower()
71+
if not slug:
72+
continue
73+
detector_cls = detectors_by_slug.get(slug)
74+
if detector_cls is None:
75+
logger.debug(
76+
"Ignoring unknown OIDC detector slug in %s: %s",
77+
DETECTOR_ORDER_ENV_VAR,
78+
slug,
79+
)
80+
continue
81+
ordered.append(detector_cls)
82+
return ordered
83+
84+
85+
def _enabled_detectors() -> list[type[EnvironmentDetector]]:
86+
"""Ordered detectors with disabled ones removed (disable always wins)."""
87+
return [
88+
detector_cls
89+
for detector_cls in _ordered_detectors()
90+
if not _is_detector_disabled(detector_cls.slug)
91+
]
92+
3493

3594
def detect_environment(
3695
context: CredentialContext,
3796
) -> EnvironmentDetector | None:
3897
"""Try each detector in order, returning the first that matches."""
39-
for detector_cls in _DETECTORS:
98+
for detector_cls in _enabled_detectors():
4099
detector = detector_cls(context=context)
41100
try:
42101
if detector.detect():

cloudsmith_cli/core/credentials/oidc/detectors/aws.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class AWSDetector(EnvironmentDetector):
2222
"""Detects AWS environments and obtains a JWT via STS GetWebIdentityToken."""
2323

2424
name = "AWS"
25+
slug = "aws"
2526

2627
def __init__(self, context):
2728
super().__init__(context)

cloudsmith_cli/core/credentials/oidc/detectors/azure_devops.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class AzureDevOpsDetector(EnvironmentDetector):
3030
"""Detects Azure DevOps and fetches an OIDC token via HTTP POST."""
3131

3232
name = "Azure DevOps"
33+
slug = "azure_devops"
3334

3435
def detect(self) -> bool:
3536
return bool(os.environ.get("SYSTEM_OIDCREQUESTURI")) and bool(

cloudsmith_cli/core/credentials/oidc/detectors/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class EnvironmentDetector:
1414
"""Base class for OIDC environment detectors."""
1515

1616
name: str = "base"
17+
slug: str = "base"
1718

1819
def __init__(self, context: CredentialContext):
1920
self.context = context

cloudsmith_cli/core/credentials/oidc/detectors/bitbucket_pipelines.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class BitbucketPipelinesDetector(EnvironmentDetector):
2020
"""Detects Bitbucket Pipelines and reads its OIDC token from environment."""
2121

2222
name = "Bitbucket Pipelines"
23+
slug = "bitbucket"
2324

2425
def detect(self) -> bool:
2526
return bool(os.environ.get("BITBUCKET_STEP_OIDC_TOKEN"))

cloudsmith_cli/core/credentials/oidc/detectors/circleci.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class CircleCIDetector(EnvironmentDetector):
2020
"""Detects CircleCI and reads OIDC token from environment variable."""
2121

2222
name = "CircleCI"
23+
slug = "circleci"
2324

2425
def detect(self) -> bool:
2526
return os.environ.get("CIRCLECI") == "true" and bool(

cloudsmith_cli/core/credentials/oidc/detectors/gcp.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class GCPDetector(EnvironmentDetector):
2828
"""Detects Google Cloud environments and obtains an OIDC ID token."""
2929

3030
name = "Google Cloud"
31+
slug = "gcp"
3132

3233
def __init__(self, context):
3334
super().__init__(context)

cloudsmith_cli/core/credentials/oidc/detectors/generic.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ class GenericDetector(EnvironmentDetector):
2727
"""
2828

2929
name = "Generic"
30+
slug = "generic"
3031

3132
def detect(self) -> bool:
3233
return bool((os.environ.get(TOKEN_ENV_VAR) or "").strip())

0 commit comments

Comments
 (0)