Skip to content
Open
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
4 changes: 3 additions & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,9 @@ disable=raw-checker-failed,
used-before-assignment,
unneeded-not,
duplicate-code,
cyclic-import
cyclic-import,
too-many-public-methods,
too-many-instance-attributes


# Enable the message, report, category or checker with the given id(s). You can
Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,30 @@ Or you can get the latest pre-release version from Cloudsmith:
pip install --upgrade cloudsmith-cli --extra-index-url=https://dl.cloudsmith.io/public/cloudsmith/cli/python/index/
```

### Optional Dependencies

The CLI supports optional extras for additional functionality:

#### AWS OIDC Support

For AWS environments (ECS, EKS, EC2), install with `aws` extra to enable automatic credential discovery:

```
pip install cloudsmith-cli[aws]
```

This installs `boto3[crt]` for AWS credential chain support, STS token generation, and AWS SSO compatibility.

#### All Optional Features

To install all optional dependencies:

```
pip install cloudsmith-cli[all]
```

**Note:** If you don't install the AWS extra, the AWS OIDC detector will gracefully skip itself with no errors.

## Configuration

There are two configuration files used by the CLI:
Expand Down
7 changes: 6 additions & 1 deletion cloudsmith_cli/cli/commands/whoami.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,12 @@ def _print_verbose_text(data):
click.echo(f" Source: {ak['source']}")
click.echo(" Note: SSO token is being used instead")
elif active == "api_key":
click.secho("Authentication Method: API Key", fg="cyan", bold=True)
if ak.get("source_key") == "oidc":
click.secho(
"Authentication Method: OIDC Auto-Discovery", fg="cyan", bold=True
)
else:
click.secho("Authentication Method: API Key", fg="cyan", bold=True)
for label, field in [
("Source", "source"),
("Token Slug", "slug"),
Expand Down
45 changes: 45 additions & 0 deletions cloudsmith_cli/cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ class Default(SectionSchema):
api_user_agent = ConfigParam(name="api_user_agent", type=str)
mcp_allowed_tools = ConfigParam(name="mcp_allowed_tools", type=str)
mcp_allowed_tool_groups = ConfigParam(name="mcp_allowed_tool_groups", type=str)
oidc_audience = ConfigParam(name="oidc_audience", type=str)
oidc_org = ConfigParam(name="oidc_org", type=str)
oidc_service_slug = ConfigParam(name="oidc_service_slug", type=str)

@matches_section("profile:*")
class Profile(Default):
Expand Down Expand Up @@ -416,6 +419,48 @@ def mcp_allowed_tool_groups(self, value):

self._set_option("mcp_allowed_tool_groups", tool_groups)

@property
def oidc_audience(self):
"""Get value for OIDC audience."""
return self._get_option("oidc_audience")

@oidc_audience.setter
def oidc_audience(self, value):
"""Set value for OIDC audience."""
self._set_option("oidc_audience", value)

@property
def oidc_org(self):
"""Get value for OIDC organisation slug."""
return self._get_option("oidc_org")

@oidc_org.setter
def oidc_org(self, value):
"""Set value for OIDC organisation slug."""
self._set_option("oidc_org", value)

@property
def oidc_service_slug(self):
"""Get value for OIDC service slug."""
return self._get_option("oidc_service_slug")

@oidc_service_slug.setter
def oidc_service_slug(self, value):
"""Set value for OIDC service slug."""
self._set_option("oidc_service_slug", value)

@property
def oidc_discovery_disabled(self):
"""Get value for OIDC discovery disabled flag."""
return self._get_option("oidc_discovery_disabled", default=False)

@oidc_discovery_disabled.setter
def oidc_discovery_disabled(self, value):
"""Set value for OIDC discovery disabled flag."""
self._set_option(
"oidc_discovery_disabled", bool(value) if value is not None else False
)

@property
def output(self):
"""Get value for output format."""
Expand Down
40 changes: 40 additions & 0 deletions cloudsmith_cli/cli/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,19 +293,59 @@ def wrapper(ctx, *args, **kwargs):
def resolve_credentials(f):
"""Resolve credentials via the provider chain. Depends on initialise_session."""

@click.option(
"--oidc-audience",
envvar="CLOUDSMITH_OIDC_AUDIENCE",
help="The OIDC audience for token requests.",
)
@click.option(
"--oidc-org",
envvar="CLOUDSMITH_ORG",
help="The Cloudsmith organisation slug for OIDC token exchange.",
)
@click.option(
"--oidc-service-slug",
envvar="CLOUDSMITH_SERVICE_SLUG",
help="The Cloudsmith service slug for OIDC token exchange.",
)
@click.option(
"--oidc-discovery-disabled",
default=None,
is_flag=True,
envvar="CLOUDSMITH_OIDC_DISCOVERY_DISABLED",
help="Disable OIDC auto-discovery.",
)
@click.pass_context
@functools.wraps(f)
def wrapper(ctx, *args, **kwargs):
# pylint: disable=missing-docstring
opts = config.get_or_create_options(ctx)

oidc_audience = kwargs.pop("oidc_audience")
oidc_org = kwargs.pop("oidc_org")
oidc_service_slug = kwargs.pop("oidc_service_slug")
oidc_discovery_disabled = _pop_boolean_flag(kwargs, "oidc_discovery_disabled")

if oidc_audience:
opts.oidc_audience = oidc_audience
if oidc_org:
opts.oidc_org = oidc_org
if oidc_service_slug:
opts.oidc_service_slug = oidc_service_slug
if oidc_discovery_disabled:
opts.oidc_discovery_disabled = oidc_discovery_disabled

context = CredentialContext(
session=opts.session,
api_key=opts.api_key,
api_host=opts.api_host or "https://api.cloudsmith.io",
creds_file_path=ctx.meta.get("creds_file"),
profile=ctx.meta.get("profile"),
debug=opts.debug,
oidc_audience=opts.oidc_audience,
oidc_org=opts.oidc_org,
oidc_service_slug=opts.oidc_service_slug,
oidc_discovery_disabled=opts.oidc_discovery_disabled,
)

chain = CredentialProviderChain()
Expand Down
9 changes: 7 additions & 2 deletions cloudsmith_cli/core/credentials/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ class CredentialContext:
profile: str | None = None
debug: bool = False
keyring_refresh_failed: bool = False
oidc_audience: str | None = None
oidc_org: str | None = None
oidc_service_slug: str | None = None
oidc_discovery_disabled: bool = False


@dataclass
Expand All @@ -57,18 +61,19 @@ class CredentialProviderChain:
"""Evaluates credential providers in order, returning the first valid result.

If no providers are given, uses the default chain:
Keyring → CLIFlag.
Keyring → CLIFlag → OIDC.
"""

def __init__(self, providers: list[CredentialProvider] | None = None):
if providers is not None:
self.providers = providers
else:
from .providers import CLIFlagProvider, KeyringProvider
from .providers import CLIFlagProvider, KeyringProvider, OidcProvider

self.providers = [
KeyringProvider(),
CLIFlagProvider(),
OidcProvider(),
]

def resolve(self, context: CredentialContext) -> CredentialResult | None:
Expand Down
6 changes: 6 additions & 0 deletions cloudsmith_cli/core/credentials/oidc/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""OIDC support for the Cloudsmith CLI credential chain.

References:
https://help.cloudsmith.io/docs/openid-connect
https://cloudsmith.com/blog/securely-connect-cloudsmith-to-your-cicd-using-oidc-authentication
"""
Loading
Loading