Skip to content

Commit eb5e56a

Browse files
remove --no-keyring flag, stick with env variable instead
1 parent c4507b8 commit eb5e56a

10 files changed

Lines changed: 234 additions & 173 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
99

1010
### Added
1111

12-
- Added `--no-keyring` flag to `cloudsmith auth` which can only be used when passing `--get-token` flag in order to skip keyring checks. This will allow users to extract their Cloudsmith API key programmatically without requiring user-input. This can also be configured on environment level by setting `CLOUDSMITH_NO_KEYRING=1`.
12+
- Added `CLOUDSMITH_NO_KEYRING` environment variable to disable keyring usage globally. Set `CLOUDSMITH_NO_KEYRING=1` to skip system keyring operations.
1313

1414
## [1.12.1] - 2026-02-03
1515

cloudsmith_cli/cli/commands/auth.py

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""CLI/Commands - Authenticate the user."""
22

3-
import os
43
import webbrowser
54

65
import click
@@ -17,16 +16,7 @@
1716
AUTH_SERVER_PORT = 12400
1817

1918

20-
def _set_no_keyring_env(ctx, param, value):
21-
"""Callback to set CLOUDSMITH_NO_KEYRING env var when flag is used."""
22-
if value:
23-
os.environ["CLOUDSMITH_NO_KEYRING"] = "1"
24-
return value
25-
26-
27-
def _perform_saml_authentication(
28-
opts, owner, enable_token_creation=False, json=False, no_keyring=False
29-
):
19+
def _perform_saml_authentication(opts, owner, enable_token_creation=False, json=False):
3020
"""Perform SAML authentication via web browser and local web server."""
3121
session = create_configured_session(opts)
3222
api_host = opts.api_config.host
@@ -50,7 +40,6 @@ def _perform_saml_authentication(
5040
debug=opts.debug,
5141
refresh_api_on_success=enable_token_creation,
5242
api_opts=opts.api_config,
53-
no_keyring=no_keyring,
5443
)
5544

5645
auth_server.handle_request()
@@ -92,21 +81,11 @@ def _perform_saml_authentication(
9281
is_flag=True,
9382
help="Output token details in json format.",
9483
)
95-
@click.option(
96-
"--no-keyring",
97-
default=False,
98-
is_flag=True,
99-
callback=_set_no_keyring_env,
100-
is_eager=True,
101-
help="Skip storing SSO tokens in system keyring. Use this in CI/CD "
102-
"environments to avoid keyring permission prompts. Note: SSO tokens "
103-
"will not persist for subsequent CLI calls.",
104-
)
10584
@decorators.common_cli_config_options
10685
@decorators.common_cli_output_options
10786
@decorators.initialise_api
10887
@click.pass_context
109-
def authenticate(ctx, opts, owner, token, force, save_config, json, no_keyring):
88+
def authenticate(ctx, opts, owner, token, force, save_config, json):
11089
"""Authenticate to Cloudsmith using the org's SAML setup."""
11190
json = json or utils.should_use_stderr(opts)
11291
# If using json output, we redirect info messages to stderr
@@ -130,7 +109,7 @@ def authenticate(ctx, opts, owner, token, force, save_config, json, no_keyring):
130109
context_message = "Failed to authenticate via SSO!"
131110
with handle_api_exceptions(ctx, opts=opts, context_msg=context_message):
132111
_perform_saml_authentication(
133-
opts, owner, enable_token_creation=token, json=json, no_keyring=no_keyring
112+
opts, owner, enable_token_creation=token, json=json
134113
)
135114

136115
if token:

cloudsmith_cli/cli/exceptions.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import click
88

99
from ..core.api.exceptions import ApiException
10-
from ..core.keyring import get_access_token
10+
from ..core.keyring import get_access_token, should_use_keyring
1111

1212

1313
@contextlib.contextmanager
@@ -170,7 +170,10 @@ def get_401_error_hint(ctx, opts, exc):
170170
"you don't have the permission to perform this action."
171171
)
172172

173-
access_token = get_access_token(opts.api_host)
173+
# Only check keyring if enabled to avoid keyring prompts
174+
access_token = None
175+
if should_use_keyring():
176+
access_token = get_access_token(opts.api_host)
174177
if access_token:
175178
return "Since you have an SSO access token set, this probably means that it has expired. Try getting a new token with 'cloudsmith auth', then try again."
176179

Lines changed: 73 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,15 @@
11
"""Tests for the webserver module."""
22

3+
import os
34
from unittest.mock import MagicMock, PropertyMock, patch
45

56
import pytest
67

7-
from ..webserver import AuthenticationWebRequestHandler, AuthenticationWebServer
8+
from ..webserver import AuthenticationWebRequestHandler
89

910

10-
class TestAuthenticationWebServer:
11-
"""Tests for AuthenticationWebServer."""
12-
13-
def test_no_keyring_attribute_set_from_kwargs(self):
14-
"""Verify no_keyring is set from kwargs."""
15-
with patch("socket.socket"):
16-
with patch.object(AuthenticationWebServer, "server_bind"):
17-
with patch.object(AuthenticationWebServer, "server_activate"):
18-
server = AuthenticationWebServer(
19-
("127.0.0.1", 12400),
20-
AuthenticationWebRequestHandler,
21-
bind_and_activate=False,
22-
owner="testorg",
23-
no_keyring=True,
24-
)
25-
assert server.no_keyring is True
26-
27-
def test_no_keyring_defaults_to_false(self):
28-
"""Verify no_keyring defaults to False when not provided."""
29-
with patch("socket.socket"):
30-
with patch.object(AuthenticationWebServer, "server_bind"):
31-
with patch.object(AuthenticationWebServer, "server_activate"):
32-
server = AuthenticationWebServer(
33-
("127.0.0.1", 12400),
34-
AuthenticationWebRequestHandler,
35-
bind_and_activate=False,
36-
owner="testorg",
37-
)
38-
assert server.no_keyring is False
39-
40-
41-
class TestAuthenticationWebRequestHandlerNoKeyring:
42-
"""Tests for AuthenticationWebRequestHandler with no_keyring flag."""
11+
class TestAuthenticationWebRequestHandlerKeyring:
12+
"""Tests for AuthenticationWebRequestHandler keyring behavior."""
4313

4414
@pytest.fixture
4515
def mock_handler(self):
@@ -57,66 +27,75 @@ def mock_handler(self):
5727
handler.debug = False
5828
return handler
5929

60-
def test_store_sso_tokens_called_when_no_keyring_false(self, mock_handler):
61-
"""Verify store_sso_tokens is called when no_keyring is False."""
62-
mock_handler.no_keyring = False
63-
64-
with patch("cloudsmith_cli.cli.webserver.store_sso_tokens") as mock_store:
65-
with patch.object(mock_handler, "_return_success_response"):
66-
with patch.object(
67-
AuthenticationWebRequestHandler,
68-
"query_data",
69-
new_callable=PropertyMock,
70-
) as mock_query:
71-
with patch.object(
72-
AuthenticationWebRequestHandler,
73-
"api_host",
74-
new_callable=PropertyMock,
75-
) as mock_host:
76-
mock_query.return_value = {
77-
"access_token": "test_access_token",
78-
"refresh_token": "test_refresh_token",
79-
}
80-
mock_host.return_value = "https://api.cloudsmith.io"
81-
82-
mock_handler.do_GET()
83-
84-
mock_store.assert_called_once_with(
85-
"https://api.cloudsmith.io",
86-
"test_access_token",
87-
"test_refresh_token",
88-
)
89-
90-
def test_store_sso_tokens_not_called_when_no_keyring_true(self, mock_handler):
91-
"""Verify store_sso_tokens is NOT called when no_keyring is True."""
92-
mock_handler.no_keyring = True
93-
94-
with patch("cloudsmith_cli.cli.webserver.store_sso_tokens") as mock_store:
95-
with patch("click.echo") as mock_echo:
96-
with patch.object(mock_handler, "_return_success_response"):
97-
with patch.object(
98-
AuthenticationWebRequestHandler,
99-
"query_data",
100-
new_callable=PropertyMock,
101-
) as mock_query:
30+
def test_store_sso_tokens_called_when_keyring_enabled(self, mock_handler):
31+
"""Verify store_sso_tokens is called when keyring is enabled."""
32+
# Ensure env var is not set
33+
env = os.environ.copy()
34+
env.pop("CLOUDSMITH_NO_KEYRING", None)
35+
36+
with patch.dict(os.environ, env, clear=True):
37+
with patch("cloudsmith_cli.cli.webserver.store_sso_tokens") as mock_store:
38+
with patch(
39+
"cloudsmith_cli.cli.webserver.should_use_keyring", return_value=True
40+
):
41+
with patch.object(mock_handler, "_return_success_response"):
10242
with patch.object(
10343
AuthenticationWebRequestHandler,
104-
"api_host",
44+
"query_data",
10545
new_callable=PropertyMock,
106-
) as mock_host:
107-
mock_query.return_value = {
108-
"access_token": "test_access_token",
109-
"refresh_token": "test_refresh_token",
110-
}
111-
mock_host.return_value = "https://api.cloudsmith.io"
112-
113-
mock_handler.do_GET()
114-
115-
# store_sso_tokens should NOT be called
116-
mock_store.assert_not_called()
117-
118-
# Message should be displayed to stderr
119-
mock_echo.assert_called_once_with(
120-
"SSO tokens not stored (--no-keyring enabled)",
121-
err=True,
122-
)
46+
) as mock_query:
47+
with patch.object(
48+
AuthenticationWebRequestHandler,
49+
"api_host",
50+
new_callable=PropertyMock,
51+
) as mock_host:
52+
mock_query.return_value = {
53+
"access_token": "test_access_token",
54+
"refresh_token": "test_refresh_token",
55+
}
56+
mock_host.return_value = "https://api.cloudsmith.io"
57+
58+
mock_handler.do_GET()
59+
60+
mock_store.assert_called_once_with(
61+
"https://api.cloudsmith.io",
62+
"test_access_token",
63+
"test_refresh_token",
64+
)
65+
66+
def test_store_sso_tokens_not_called_when_keyring_disabled(self, mock_handler):
67+
"""Verify store_sso_tokens is NOT called when CLOUDSMITH_NO_KEYRING=1."""
68+
with patch.dict(os.environ, {"CLOUDSMITH_NO_KEYRING": "1"}):
69+
with patch("cloudsmith_cli.cli.webserver.store_sso_tokens") as mock_store:
70+
with patch(
71+
"cloudsmith_cli.cli.webserver.should_use_keyring",
72+
return_value=False,
73+
):
74+
with patch("click.echo") as mock_echo:
75+
with patch.object(mock_handler, "_return_success_response"):
76+
with patch.object(
77+
AuthenticationWebRequestHandler,
78+
"query_data",
79+
new_callable=PropertyMock,
80+
) as mock_query:
81+
with patch.object(
82+
AuthenticationWebRequestHandler,
83+
"api_host",
84+
new_callable=PropertyMock,
85+
) as mock_host:
86+
mock_query.return_value = {
87+
"access_token": "test_access_token",
88+
"refresh_token": "test_refresh_token",
89+
}
90+
mock_host.return_value = "https://api.cloudsmith.io"
91+
92+
mock_handler.do_GET()
93+
94+
# store_sso_tokens should NOT be called
95+
mock_store.assert_not_called()
96+
97+
# Message should be displayed to stderr
98+
mock_echo.assert_called_once_with(
99+
"SSO tokens not stored (CLOUDSMITH_NO_KEYRING is set)",
100+
err=True,
101+
)

cloudsmith_cli/cli/webserver.py

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from ..core.api.exceptions import ApiException
1111
from ..core.api.init import initialise_api
12-
from ..core.keyring import store_sso_tokens
12+
from ..core.keyring import should_use_keyring, store_sso_tokens
1313
from .saml import exchange_2fa_token
1414

1515

@@ -52,7 +52,6 @@ def __init__(
5252
self.debug = kwargs.get("debug", False)
5353
self.refresh_api_on_success = kwargs.get("refresh_api_on_success", False)
5454
self.api_opts = kwargs.get("api_opts")
55-
self.no_keyring = kwargs.get("no_keyring", False)
5655
self.exception = None
5756

5857
super().__init__(
@@ -91,7 +90,6 @@ def finish_request(self, request, client_address):
9190
session=self.session,
9291
refresh_api_on_success=self.refresh_api_on_success,
9392
server_instance=self,
94-
no_keyring=self.no_keyring,
9593
)
9694

9795
def _handle_request_noblock(self):
@@ -135,7 +133,6 @@ def __init__(self, request, client_address, server, **kwargs):
135133
self.session = kwargs.get("session")
136134
self.refresh_api_on_success = kwargs.get("refresh_api_on_success", False)
137135
self.server_instance = kwargs.get("server_instance")
138-
self.no_keyring = kwargs.get("no_keyring", False)
139136

140137
super().__init__(request, client_address, server)
141138

@@ -203,17 +200,17 @@ def do_GET(self):
203200

204201
try:
205202
if access_token:
206-
if self.no_keyring:
207-
click.echo(
208-
"SSO tokens not stored (--no-keyring enabled)",
209-
err=True,
210-
)
211-
else:
203+
if should_use_keyring():
212204
store_sso_tokens(
213205
self.api_host,
214206
access_token,
215207
refresh_token,
216208
)
209+
else:
210+
click.echo(
211+
"SSO tokens not stored (CLOUDSMITH_NO_KEYRING is set)",
212+
err=True,
213+
)
217214

218215
if self.refresh_api_on_success and self.server_instance:
219216
self.server_instance.refresh_api_config_after_auth()
@@ -229,17 +226,17 @@ def do_GET(self):
229226
access_token, refresh_token = exchange_2fa_token(
230227
self.api_host, two_factor_token, totp_token, session=self.session
231228
)
232-
if self.no_keyring:
233-
click.echo(
234-
"SSO tokens not stored (--no-keyring enabled)",
235-
err=True,
236-
)
237-
else:
229+
if should_use_keyring():
238230
store_sso_tokens(
239231
self.api_host,
240232
access_token,
241233
refresh_token,
242234
)
235+
else:
236+
click.echo(
237+
"SSO tokens not stored (CLOUDSMITH_NO_KEYRING is set)",
238+
err=True,
239+
)
243240

244241
if self.refresh_api_on_success and self.server_instance:
245242
self.server_instance.refresh_api_config_after_auth()

0 commit comments

Comments
 (0)