Skip to content

Commit 19d1519

Browse files
feat: add OIDC detector enable/order controls
Add two controls for OIDC detector selection, resolved through the credential context rather than read ad hoc from the environment: - CLOUDSMITH_OIDC_<ID>_DISABLED skips a specific detector (only the literal "true", case-insensitive, disables). The credentials decorator resolves these into context.oidc_disabled_detectors. - --oidc-detector-order (env var CLOUDSMITH_OIDC_DETECTOR_ORDER) overrides which detectors are considered and the order they are tried in (comma-separated ids; unlisted/unknown ids are skipped). When both are set the order list defines the candidate set and sequence, then the disabled set is applied on top, so a disabled detector is always skipped. Each detector gains a stable `id` attribute and a public `registered_detectors()` accessor is added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 792d9d6 commit 19d1519

15 files changed

Lines changed: 246 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1616
- Added GitHub Actions to OIDC credential auto-discovery. When running in GitHub Actions (with `id-token: write` permission), the CLI fetches an OIDC token from the Actions runtime endpoint and exchanges it for a Cloudsmith access token. Works out of the box with no extra dependencies.
1717
- 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.
1818
- 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`) and exchanges it for a Cloudsmith access token. Works out of the box with no extra dependencies.
19+
- Added controls for OIDC detector selection. Set `CLOUDSMITH_OIDC_<DETECTOR>_DISABLED=true` to skip a specific detector (only the literal `true` disables), or use `--oidc-detector-order` (env var `CLOUDSMITH_OIDC_DETECTOR_ORDER`) with a comma-separated list of detector ids 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 ids: `aws`, `azure_devops`, `bitbucket`, `circleci`, `generic`, `github`, `gitlab`.
1920

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

README.md

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

212212
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).
213213

214+
#### Controlling OIDC Detector Selection
215+
216+
By default the CLI tries each detector in a fixed priority order and uses the first that matches. Two controls 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):
217+
218+
- **Disable a detector** — set `CLOUDSMITH_OIDC_<DETECTOR>_DISABLED=true` to skip it entirely. 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.
219+
- **Reorder evaluation** — use `--oidc-detector-order` (or the `CLOUDSMITH_OIDC_DETECTOR_ORDER` environment variable) with a comma-separated list of detector ids to control both which detectors are considered and the order they are tried in (first match wins). Ids not listed are skipped; unrecognised ids are ignored. For example, `--oidc-detector-order=generic,aws` tries the generic detector first and considers only those two.
220+
221+
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 ids are: `aws`, `azure_devops`, `bitbucket`, `circleci`, `generic`, `github`, `gitlab`.
222+
214223
## Configuration
215224

216225
There are two configuration files used by the CLI:

cloudsmith_cli/cli/config.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,16 @@ def oidc_discovery_disabled(self, value):
489489
"oidc_discovery_disabled", bool(value) if value is not None else False
490490
)
491491

492+
@property
493+
def oidc_detector_order(self):
494+
"""Get value for the OIDC detector evaluation order."""
495+
return self._get_option("oidc_detector_order")
496+
497+
@oidc_detector_order.setter
498+
def oidc_detector_order(self, value):
499+
"""Set value for the OIDC detector evaluation order."""
500+
self._set_option("oidc_detector_order", value)
501+
492502
@property
493503
def metadata_failure_mode(self):
494504
"""Get value for push-time metadata failure mode."""

cloudsmith_cli/cli/decorators.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""CLI - Decorators."""
22

33
import functools
4+
import os
45

56
import click
67
from click.core import ParameterSource
@@ -10,6 +11,7 @@
1011
from ..core.api.init import initialise_api as _initialise_api
1112
from ..core.credentials.chain import CredentialProviderChain
1213
from ..core.credentials.models import CredentialContext
14+
from ..core.credentials.oidc.detectors import disabled_detectors_from_env
1315
from ..core.mcp import server
1416
from ..core.rest import create_requests_session as _create_session
1517
from . import config, utils
@@ -332,6 +334,12 @@ def resolve_credentials(f):
332334
envvar="CLOUDSMITH_OIDC_DISCOVERY_DISABLED",
333335
help="Disable OIDC auto-discovery.",
334336
)
337+
@click.option(
338+
"--oidc-detector-order",
339+
envvar="CLOUDSMITH_OIDC_DETECTOR_ORDER",
340+
help="Comma-separated OIDC detector ids to control which detectors "
341+
"are considered and the order they are tried in.",
342+
)
335343
@click.pass_context
336344
@functools.wraps(f)
337345
def wrapper(ctx, *args, **kwargs):
@@ -342,6 +350,7 @@ def wrapper(ctx, *args, **kwargs):
342350
oidc_org = kwargs.pop("oidc_org")
343351
oidc_service_slug = kwargs.pop("oidc_service_slug")
344352
oidc_discovery_disabled = _pop_boolean_flag(kwargs, "oidc_discovery_disabled")
353+
oidc_detector_order = kwargs.pop("oidc_detector_order")
345354

346355
if oidc_audience:
347356
opts.oidc_audience = oidc_audience
@@ -351,6 +360,8 @@ def wrapper(ctx, *args, **kwargs):
351360
opts.oidc_service_slug = oidc_service_slug
352361
if oidc_discovery_disabled:
353362
opts.oidc_discovery_disabled = oidc_discovery_disabled
363+
if oidc_detector_order:
364+
opts.oidc_detector_order = oidc_detector_order
354365

355366
context = CredentialContext(
356367
session=opts.session,
@@ -365,6 +376,8 @@ def wrapper(ctx, *args, **kwargs):
365376
oidc_org=opts.oidc_org,
366377
oidc_service_slug=opts.oidc_service_slug,
367378
oidc_discovery_disabled=opts.oidc_discovery_disabled,
379+
oidc_detector_order=opts.oidc_detector_order,
380+
oidc_disabled_detectors=disabled_detectors_from_env(os.environ),
368381
)
369382

370383
chain = CredentialProviderChain()

cloudsmith_cli/core/credentials/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ class CredentialContext:
3030
oidc_org: str | None = None
3131
oidc_service_slug: str | None = None
3232
oidc_discovery_disabled: bool = False
33+
oidc_detector_order: str | None = None
34+
oidc_disabled_detectors: frozenset[str] = frozenset()
3335

3436

3537
@dataclass

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

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
from .gitlab_ci import GitLabCIDetector
1616

1717
if TYPE_CHECKING:
18+
from collections.abc import Mapping
19+
1820
from ... import CredentialContext
1921

2022
logger = logging.getLogger(__name__)
@@ -30,11 +32,74 @@
3032
]
3133

3234

35+
def registered_detectors() -> list[type[EnvironmentDetector]]:
36+
"""Return the registered OIDC detectors in their default priority order."""
37+
return list(_DETECTORS)
38+
39+
40+
def disable_env_var(identifier: str) -> str:
41+
"""The environment variable that disables the detector with this id."""
42+
return f"CLOUDSMITH_OIDC_{identifier.upper()}_DISABLED"
43+
44+
45+
def _is_disabled_value(value: str | None) -> bool:
46+
"""Only the literal string ``true`` (case-insensitive) disables a detector."""
47+
return (value or "").strip().lower() == "true"
48+
49+
50+
def disabled_detectors_from_env(environ: Mapping[str, str]) -> frozenset[str]:
51+
"""Detector ids disabled via their CLOUDSMITH_OIDC_<ID>_DISABLED variable."""
52+
return frozenset(
53+
detector_cls.id
54+
for detector_cls in _DETECTORS
55+
if _is_disabled_value(environ.get(disable_env_var(detector_cls.id)))
56+
)
57+
58+
59+
def _ordered_detectors(order: str | None) -> list[type[EnvironmentDetector]]:
60+
"""Detectors in evaluation order, honouring an explicit order string.
61+
62+
When ``order`` is unset/empty the default registration order is used.
63+
Otherwise only the listed ids are considered, in the listed order;
64+
unknown ids are logged and skipped.
65+
"""
66+
raw_order = (order or "").strip()
67+
if not raw_order:
68+
return list(_DETECTORS)
69+
70+
detectors_by_id = {detector_cls.id: detector_cls for detector_cls in _DETECTORS}
71+
ordered: list[type[EnvironmentDetector]] = []
72+
for token in raw_order.split(","):
73+
identifier = token.strip().lower()
74+
if not identifier:
75+
continue
76+
detector_cls = detectors_by_id.get(identifier)
77+
if detector_cls is None:
78+
logger.debug("Ignoring unknown OIDC detector id: %s", identifier)
79+
continue
80+
ordered.append(detector_cls)
81+
return ordered
82+
83+
84+
def _enabled_detectors(
85+
order: str | None, disabled: frozenset[str]
86+
) -> list[type[EnvironmentDetector]]:
87+
"""Ordered detectors with disabled ones removed (disable always wins)."""
88+
return [
89+
detector_cls
90+
for detector_cls in _ordered_detectors(order)
91+
if detector_cls.id not in disabled
92+
]
93+
94+
3395
def detect_environment(
3496
context: CredentialContext,
3597
) -> EnvironmentDetector | None:
3698
"""Try each detector in order, returning the first that matches."""
37-
for detector_cls in _DETECTORS:
99+
enabled = _enabled_detectors(
100+
context.oidc_detector_order, context.oidc_disabled_detectors
101+
)
102+
for detector_cls in enabled:
38103
detector = detector_cls(context=context)
39104
try:
40105
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
@@ -24,6 +24,7 @@ class AWSDetector(EnvironmentDetector):
2424
"""Detects AWS environments and obtains a JWT via STS GetWebIdentityToken."""
2525

2626
name = "AWS"
27+
id = "aws"
2728

2829
def __init__(self, context):
2930
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+
id = "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
@@ -12,6 +12,7 @@ class EnvironmentDetector:
1212
"""Base class for OIDC environment detectors."""
1313

1414
name: str = "base"
15+
id: str = "base"
1516

1617
def __init__(self, context: CredentialContext):
1718
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+
id = "bitbucket"
2324

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

0 commit comments

Comments
 (0)