Skip to content

Commit 61ee9b3

Browse files
add --request-api-key flag and deprecation notices for --force and --token command
1 parent f11a1f3 commit 61ee9b3

9 files changed

Lines changed: 674 additions & 45 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1010
### Added
1111

1212
- Added `CLOUDSMITH_NO_KEYRING` environment variable to disable keyring usage globally. Set `CLOUDSMITH_NO_KEYRING=1` to skip system keyring operations.
13+
- Added `--request-api-key` flag to `cloudsmith auth` command for fully automated, non-interactive API token retrieval. Auto-creates a token if none exists, or auto-rotates (with warning) if one already exists. Compatible with `--save-config` and `CLOUDSMITH_NO_KEYRING`.
14+
15+
### Deprecation Notices
16+
17+
- The `--token` flag on `cloudsmith auth` will be deprecated. Use `--request-api-key` instead.
18+
- The `--force` flag on `cloudsmith auth` will be deprecated. Use `--request-api-key` instead (force behavior is implied).
19+
- The `--json` flag on `cloudsmith auth` will be deprecated. Use `--output-format json` instead.
1320

1421
## [1.12.1] - 2026-02-03
1522

cloudsmith_cli/cli/commands/auth.py

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@
99
from ..saml import create_configured_session, get_idp_url
1010
from ..webserver import AuthenticationWebRequestHandler, AuthenticationWebServer
1111
from .main import main
12-
from .tokens import create
12+
from .tokens import create, request_api_key
1313

1414
# Authentication server configuration
1515
AUTH_SERVER_HOST = "127.0.0.1"
1616
AUTH_SERVER_PORT = 12400
1717

1818

19-
def _perform_saml_authentication(opts, owner, enable_token_creation=False, json=False):
19+
def _perform_saml_authentication(
20+
opts, owner, enable_token_creation=False, use_stderr=False
21+
):
2022
"""Perform SAML authentication via web browser and local web server."""
2123
session = create_configured_session(opts)
2224
api_host = opts.api_config.host
@@ -25,12 +27,12 @@ def _perform_saml_authentication(opts, owner, enable_token_creation=False, json=
2527

2628
click.echo(
2729
f"Opening your organization's SAML IDP URL in your browser: {click.style(idp_url, bold=True)}",
28-
err=json,
30+
err=use_stderr,
2931
)
30-
click.echo(err=json)
32+
click.echo(err=use_stderr)
3133
webbrowser.open(idp_url)
3234

33-
click.echo("Starting webserver to begin authentication ... ", err=json)
35+
click.echo("Starting webserver to begin authentication ... ", err=use_stderr)
3436

3537
auth_server = AuthenticationWebServer(
3638
(AUTH_SERVER_HOST, AUTH_SERVER_PORT),
@@ -60,14 +62,14 @@ def _perform_saml_authentication(opts, owner, enable_token_creation=False, json=
6062
"--token",
6163
default=False,
6264
is_flag=True,
63-
help="Retrieve a user API token after successful authentication.",
65+
help="[DEPRECATED: Use --request-api-key] Retrieve a user API token after successful authentication.",
6466
)
6567
@click.option(
6668
"-f",
6769
"--force",
6870
default=False,
6971
is_flag=True,
70-
help="Force refresh of user API token without prompts.",
72+
help="[DEPRECATED: Use --request-api-key] Force refresh of user API token without prompts.",
7173
)
7274
@click.option(
7375
"--save-config",
@@ -81,15 +83,47 @@ def _perform_saml_authentication(opts, owner, enable_token_creation=False, json=
8183
is_flag=True,
8284
help="Output token details in json format.",
8385
)
86+
@click.option(
87+
"--request-api-key",
88+
"request_api_key_flag",
89+
default=False,
90+
is_flag=True,
91+
help="Retrieve API token (auto-creates or auto-rotates, no prompts). "
92+
"Warning: If token exists, this will rotate it and invalidate the old key.",
93+
)
8494
@decorators.common_cli_config_options
8595
@decorators.common_cli_output_options
8696
@decorators.initialise_api
8797
@click.pass_context
88-
def authenticate(ctx, opts, owner, token, force, save_config, json):
98+
def authenticate(
99+
ctx, opts, owner, token, force, save_config, json, request_api_key_flag
100+
):
89101
"""Authenticate to Cloudsmith using the org's SAML setup."""
90-
json = json or utils.should_use_stderr(opts)
91-
# If using json output, we redirect info messages to stderr
92-
use_stderr = json
102+
# Validate mutual exclusivity
103+
if request_api_key_flag and (token or force):
104+
raise click.UsageError(
105+
"--request-api-key cannot be used with --token or --force. "
106+
"Use --request-api-key alone for fully automated token retrieval."
107+
)
108+
109+
# Determine if we should redirect info messages to stderr
110+
use_stderr = request_api_key_flag or json or utils.should_use_stderr(opts)
111+
112+
if token:
113+
click.secho(
114+
"DEPRECATION WARNING: The `--token` flag is deprecated and will be removed in a future release. "
115+
"Please use `--request-api-key` instead.",
116+
fg="yellow",
117+
err=True,
118+
)
119+
120+
if force:
121+
click.secho(
122+
"DEPRECATION WARNING: The `--force` flag is deprecated and will be removed in a future release. "
123+
"Please use `--request-api-key` instead (force is implied).",
124+
fg="yellow",
125+
err=True,
126+
)
93127

94128
if json and not utils.should_use_stderr(opts):
95129
click.secho(
@@ -106,11 +140,34 @@ def authenticate(ctx, opts, owner, token, force, save_config, json):
106140
err=use_stderr,
107141
)
108142

143+
# Determine if we need to refresh API after SSO (required for token operations)
144+
enable_token_creation = token or request_api_key_flag
145+
109146
context_message = "Failed to authenticate via SSO!"
110147
with handle_api_exceptions(ctx, opts=opts, context_msg=context_message):
111148
_perform_saml_authentication(
112-
opts, owner, enable_token_creation=token, json=json
149+
opts,
150+
owner,
151+
enable_token_creation=enable_token_creation,
152+
use_stderr=use_stderr,
113153
)
114154

155+
if request_api_key_flag:
156+
# Non-interactive token retrieval
157+
new_token = request_api_key(ctx, opts, save_config=save_config)
158+
159+
if not new_token:
160+
raise click.ClickException(
161+
"Failed to retrieve API token. No token was returned."
162+
)
163+
164+
# Check if JSON output is requested
165+
if utils.maybe_print_as_json(opts, new_token):
166+
return
167+
168+
# Default: output only the raw token value to stdout
169+
click.echo(new_token.key)
170+
return
171+
115172
if token:
116173
ctx.invoke(create, opts=opts, save_config=save_config, force=force, json=json)

cloudsmith_cli/cli/commands/tokens.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,77 @@ def handle_duplicate_token_error(exc, ctx, opts, save_config, force, json):
3131
raise exc
3232

3333

34+
def request_api_key(ctx, opts, save_config=False):
35+
"""
36+
Request an API key non-interactively.
37+
38+
This function creates a new token or rotates an existing one without any prompts.
39+
Used by the --request-api-key flag in the auth command.
40+
41+
Returns the token object on success.
42+
Raises ApiException on failure.
43+
"""
44+
context_msg = "Failed to retrieve API token!"
45+
46+
try:
47+
# Don't use handle_api_exceptions here so we can catch and handle
48+
# the "already has token" error ourselves
49+
with utils.maybe_spinner(opts):
50+
new_token = api.create_user_token_saml()
51+
52+
if save_config:
53+
create, has_errors = create_config_files(
54+
ctx, opts, api_key=new_token.key, force=True
55+
)
56+
new_config_messaging(has_errors, opts, create, api_key=new_token.key)
57+
58+
return new_token
59+
60+
except exceptions.ApiException as exc:
61+
if exc.status == 401:
62+
# Unauthorized - re-raise with handler
63+
with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
64+
raise
65+
66+
if (
67+
exc.status == 400
68+
and exc.detail
69+
and "User has already created an API key" in exc.detail
70+
):
71+
# Token exists - rotate it automatically
72+
click.echo(
73+
"Warning: Rotating existing API token. Your old key will be invalidated.",
74+
err=True,
75+
)
76+
77+
# List tokens and select the first one
78+
with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
79+
with utils.maybe_spinner(opts):
80+
api_tokens = api.list_user_tokens()
81+
82+
if not api_tokens:
83+
raise click.ClickException("No existing tokens found to rotate.")
84+
85+
token_slug = api_tokens[0].slug_perm
86+
87+
# Refresh the token
88+
with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
89+
with utils.maybe_spinner(opts):
90+
new_token = api.refresh_user_token(token_slug)
91+
92+
if save_config:
93+
create, has_errors = create_config_files(
94+
ctx, opts, api_key=new_token.key, force=True
95+
)
96+
new_config_messaging(has_errors, opts, create, api_key=new_token.key)
97+
98+
return new_token
99+
100+
# Other errors - use the handler
101+
with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
102+
raise
103+
104+
34105
@main.group(cls=command.AliasGroup, name="tokens")
35106
@decorators.common_cli_config_options
36107
@decorators.common_cli_output_options

0 commit comments

Comments
 (0)