Skip to content

Commit 7906cdc

Browse files
feat: support OIDC detector controls in config file + warn on misconfig
Address PR #311 review feedback: - Allow oidc_detector_order and oidc_disabled_detectors to be set in config.ini (under [default] or a profile). The config disabled list is additive with the per-detector CLOUDSMITH_OIDC_<ID>_DISABLED env vars; the --oidc-detector-order flag / env var still override the config order. - Surface a warning (in the CLI layer, where click lives) when --oidc-detector-order names unknown ids, or when the order/disable controls leave no detector enabled. Advisory only: the credential fallback chain is preserved rather than aborting with a UsageError, and the core detectors module stays click-free. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 61dc52d commit 7906cdc

5 files changed

Lines changed: 157 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +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`.
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. Both controls can also be set in `config.ini` via the `oidc_detector_order` and `oidc_disabled_detectors` keys (the latter additive with the `*_DISABLED` env vars). Unknown ids in the order, or controls that leave no detector enabled, are surfaced as a warning. Detector ids: `aws`, `azure_devops`, `bitbucket`, `circleci`, `generic`, `github`, `gitlab`.
2020

2121
## [1.18.0] - 2026-06-09
2222

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,10 +216,20 @@ As a fallback for environments without a dedicated detector (for example Jenkins
216216
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):
217217

218218
- **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.
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 with a warning. For example, `--oidc-detector-order=generic,aws` tries the generic detector first and considers only those two.
220220

221221
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`.
222222

223+
Both controls can also be set in `config.ini`, under `[default]` or a profile section:
224+
225+
```ini
226+
[default]
227+
oidc_detector_order = github, aws
228+
oidc_disabled_detectors = aws, gitlab
229+
```
230+
231+
The `--oidc-detector-order` flag (or the `CLOUDSMITH_OIDC_DETECTOR_ORDER` environment variable) overrides the `oidc_detector_order` config value. The `oidc_disabled_detectors` config key is additive with the per-detector `CLOUDSMITH_OIDC_<DETECTOR>_DISABLED` environment variables — a detector disabled by either is skipped.
232+
223233
## Configuration
224234

225235
There are two configuration files used by the CLI:

cloudsmith_cli/cli/config.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ class Default(SectionSchema):
6969
oidc_audience = ConfigParam(name="oidc_audience", type=str)
7070
oidc_org = ConfigParam(name="oidc_org", type=str)
7171
oidc_service_slug = ConfigParam(name="oidc_service_slug", type=str)
72+
oidc_detector_order = ConfigParam(name="oidc_detector_order", type=str)
73+
oidc_disabled_detectors = ConfigParam(name="oidc_disabled_detectors", type=str)
7274
metadata_failure_mode = ConfigParam(name="metadata_failure_mode", type=str)
7375

7476
@matches_section("profile:*")
@@ -499,6 +501,16 @@ def oidc_detector_order(self, value):
499501
"""Set value for the OIDC detector evaluation order."""
500502
self._set_option("oidc_detector_order", value)
501503

504+
@property
505+
def oidc_disabled_detectors(self):
506+
"""Get the comma-separated OIDC detector ids disabled via config."""
507+
return self._get_option("oidc_disabled_detectors")
508+
509+
@oidc_disabled_detectors.setter
510+
def oidc_disabled_detectors(self, value):
511+
"""Set the comma-separated OIDC detector ids disabled via config."""
512+
self._set_option("oidc_disabled_detectors", value)
513+
502514
@property
503515
def metadata_failure_mode(self):
504516
"""Get value for push-time metadata failure mode."""

cloudsmith_cli/cli/decorators.py

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
from ..core.api.init import initialise_api as _initialise_api
1212
from ..core.credentials.chain import CredentialProviderChain
1313
from ..core.credentials.models import CredentialContext
14-
from ..core.credentials.oidc.detectors import disabled_detectors_from_env
14+
from ..core.credentials.oidc.detectors import (
15+
disabled_detectors_from_env,
16+
registered_detectors,
17+
)
1518
from ..core.mcp import server
1619
from ..core.rest import create_requests_session as _create_session
1720
from . import config, utils
@@ -309,6 +312,50 @@ def wrapper(ctx, *args, **kwargs):
309312
return wrapper
310313

311314

315+
def _parse_detector_ids(value):
316+
"""Split a comma-separated detector id string into stripped, lowercased ids."""
317+
return [
318+
token.strip().lower() for token in (value or "").split(",") if token.strip()
319+
]
320+
321+
322+
def _config_disabled_detectors(value):
323+
"""Detector ids disabled via the config-file ``oidc_disabled_detectors`` key."""
324+
return frozenset(_parse_detector_ids(value))
325+
326+
327+
def _warn_on_oidc_detector_controls(order, disabled):
328+
"""Warn when the OIDC detector order/disable controls look misconfigured.
329+
330+
Advisory only — detection itself is resolved in the core detectors module;
331+
these warnings surface user-facing mistakes (a typo'd id, or controls that
332+
leave nothing enabled) without aborting the credential fallback chain.
333+
"""
334+
valid_ids = [detector_cls.id for detector_cls in registered_detectors()]
335+
order_ids = _parse_detector_ids(order)
336+
337+
unknown_ids = [
338+
identifier for identifier in order_ids if identifier not in valid_ids
339+
]
340+
if unknown_ids:
341+
click.secho(
342+
"OIDC: ignoring unknown detector id(s) in --oidc-detector-order: "
343+
f"{', '.join(unknown_ids)}. Valid ids: {', '.join(valid_ids)}.",
344+
fg="yellow",
345+
err=True,
346+
)
347+
348+
candidate_ids = [i for i in order_ids if i in valid_ids] if order_ids else valid_ids
349+
enabled_ids = [i for i in candidate_ids if i not in disabled]
350+
if (order_ids or disabled) and not enabled_ids:
351+
click.secho(
352+
"OIDC: no detectors are enabled after applying the detector "
353+
"order/disable controls; OIDC auto-discovery will be skipped.",
354+
fg="yellow",
355+
err=True,
356+
)
357+
358+
312359
def resolve_credentials(f):
313360
"""Resolve credentials via the provider chain. Depends on initialise_session."""
314361

@@ -363,6 +410,13 @@ def wrapper(ctx, *args, **kwargs):
363410
if oidc_detector_order:
364411
opts.oidc_detector_order = oidc_detector_order
365412

413+
oidc_disabled_detectors = disabled_detectors_from_env(
414+
os.environ
415+
) | _config_disabled_detectors(opts.oidc_disabled_detectors)
416+
_warn_on_oidc_detector_controls(
417+
opts.oidc_detector_order, oidc_disabled_detectors
418+
)
419+
366420
context = CredentialContext(
367421
session=opts.session,
368422
api_key_from_flag=opts.api_key_from_flag,
@@ -377,7 +431,7 @@ def wrapper(ctx, *args, **kwargs):
377431
oidc_service_slug=opts.oidc_service_slug,
378432
oidc_discovery_disabled=opts.oidc_discovery_disabled,
379433
oidc_detector_order=opts.oidc_detector_order,
380-
oidc_disabled_detectors=disabled_detectors_from_env(os.environ),
434+
oidc_disabled_detectors=oidc_disabled_detectors,
381435
)
382436

383437
chain = CredentialProviderChain()
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Copyright 2026 Cloudsmith Ltd
2+
"""Tests for config-file + CLI-layer OIDC detector controls."""
3+
4+
import pytest
5+
6+
from ..config import ConfigReader, Options
7+
from ..decorators import _config_disabled_detectors, _warn_on_oidc_detector_controls
8+
9+
10+
@pytest.fixture
11+
def config_file(tmp_path):
12+
"""Yield a writer for a temporary config.ini, restoring reader state."""
13+
original_files = list(ConfigReader.config_files)
14+
15+
def write(body):
16+
path = tmp_path / "config.ini"
17+
path.write_text(body)
18+
return str(path)
19+
20+
yield write
21+
ConfigReader.config_files = original_files
22+
23+
24+
class TestConfigFileControls:
25+
def test_detector_order_is_read_from_config(self, config_file):
26+
path = config_file("[default]\noidc_detector_order = github, aws\n")
27+
opts = Options()
28+
opts.load_config_file(path)
29+
assert opts.oidc_detector_order == "github, aws"
30+
31+
def test_disabled_detectors_is_read_from_config(self, config_file):
32+
path = config_file("[default]\noidc_disabled_detectors = aws,gitlab\n")
33+
opts = Options()
34+
opts.load_config_file(path)
35+
assert opts.oidc_disabled_detectors == "aws,gitlab"
36+
37+
38+
class TestConfigDisabledDetectors:
39+
def test_parses_comma_separated_ids(self):
40+
assert _config_disabled_detectors("aws, gitlab") == frozenset({"aws", "gitlab"})
41+
42+
def test_lowercases_and_strips(self):
43+
assert _config_disabled_detectors(" AWS , GitLab ") == frozenset(
44+
{"aws", "gitlab"}
45+
)
46+
47+
def test_empty_or_none_is_empty_set(self):
48+
assert _config_disabled_detectors(None) == frozenset()
49+
assert _config_disabled_detectors(" ") == frozenset()
50+
51+
52+
class TestDetectorControlWarnings:
53+
def test_unknown_id_warns(self, capsys):
54+
_warn_on_oidc_detector_controls("github,nope", frozenset())
55+
err = capsys.readouterr().err
56+
assert "nope" in err
57+
assert "github" not in err.split("Valid ids", 1)[0]
58+
59+
def test_no_warning_when_all_ids_known(self, capsys):
60+
_warn_on_oidc_detector_controls("github,aws", frozenset())
61+
assert capsys.readouterr().err == ""
62+
63+
def test_no_warning_without_controls(self, capsys):
64+
_warn_on_oidc_detector_controls(None, frozenset())
65+
assert capsys.readouterr().err == ""
66+
67+
def test_warns_when_no_detectors_enabled_via_order(self, capsys):
68+
_warn_on_oidc_detector_controls("nope", frozenset())
69+
assert "no detectors are enabled" in capsys.readouterr().err.lower()
70+
71+
def test_warns_when_order_fully_disabled(self, capsys):
72+
_warn_on_oidc_detector_controls("aws", frozenset({"aws"}))
73+
assert "no detectors are enabled" in capsys.readouterr().err.lower()
74+
75+
def test_no_enabled_warning_in_normal_case(self, capsys):
76+
_warn_on_oidc_detector_controls("github,aws", frozenset())
77+
assert "no detectors are enabled" not in capsys.readouterr().err.lower()

0 commit comments

Comments
 (0)