cloudsmith-cli 1.12.1__tar.gz → 1.13.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cloudsmith_cli-1.12.1/cloudsmith_cli.egg-info → cloudsmith_cli-1.13.0}/PKG-INFO +3 -2
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/README.md +1 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/__init__.py +1 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/auth.py +70 -13
- cloudsmith_cli-1.13.0/cloudsmith_cli/cli/commands/logout.py +151 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/tokens.py +71 -0
- cloudsmith_cli-1.13.0/cloudsmith_cli/cli/commands/whoami.py +193 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/config.py +38 -0
- cloudsmith_cli-1.13.0/cloudsmith_cli/cli/tests/commands/conftest.py +27 -0
- cloudsmith_cli-1.13.0/cloudsmith_cli/cli/tests/commands/test_auth.py +283 -0
- cloudsmith_cli-1.13.0/cloudsmith_cli/cli/tests/commands/test_logout.py +147 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/test_tokens.py +75 -8
- cloudsmith_cli-1.13.0/cloudsmith_cli/cli/tests/test_webserver.py +155 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/webserver.py +23 -10
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/init.py +34 -16
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/user.py +10 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/config.py +22 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/download.py +1 -1
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/keyring.py +51 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/mcp/server.py +4 -10
- cloudsmith_cli-1.13.0/cloudsmith_cli/core/tests/test_init.py +371 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/tests/test_keyring.py +99 -5
- cloudsmith_cli-1.13.0/cloudsmith_cli/data/VERSION +1 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0/cloudsmith_cli.egg-info}/PKG-INFO +3 -2
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli.egg-info/SOURCES.txt +5 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli.egg-info/requires.txt +1 -1
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/setup.py +1 -1
- cloudsmith_cli-1.12.1/cloudsmith_cli/cli/commands/whoami.py +0 -62
- cloudsmith_cli-1.12.1/cloudsmith_cli/core/tests/test_init.py +0 -186
- cloudsmith_cli-1.12.1/cloudsmith_cli/data/VERSION +0 -1
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/LICENSE +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/MANIFEST.in +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/__init__.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/__main__.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/__init__.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/command.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/check.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/copy.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/delete.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/dependencies.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/docs.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/download.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/entitlements.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/help_.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/list_.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/login.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/main.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/mcp.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/metrics/__init__.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/metrics/command.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/metrics/entitlements.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/metrics/packages.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/move.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/policy/__init__.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/policy/command.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/policy/deny.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/policy/license.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/policy/vulnerability.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/push.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/quarantine.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/quota/__init__.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/quota/command.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/quota/history.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/quota/quota.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/repos.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/resync.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/status.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/tags.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/upstream.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/decorators.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/exceptions.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/saml.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/table.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/__init__.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/__init__.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/policy/__init__.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/policy/test_deny.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/policy/test_licence.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/policy/test_vulnerability.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/test_check.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/test_download.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/test_entitlements.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/test_login.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/test_main.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/test_mcp.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/test_package_commands.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/test_repos.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/test_upstream.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/conftest.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/test_push.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/test_saml.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/test_utils.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/utils.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/types.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/utils.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/validators.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/__init__.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/__init__.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/distros.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/entitlements.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/exceptions.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/files.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/metrics.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/orgs.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/packages.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/quota.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/rates.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/repos.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/status.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/upstreams.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/version.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/mcp/__init__.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/mcp/data.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/pagination.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/ratelimits.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/rest.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/tests/__init__.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/tests/test_download.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/tests/test_rest.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/tests/test_version.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/utils.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/version.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/data/config.ini +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/data/credentials.ini +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/templates/__init__.py +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/templates/auth_error.html +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/templates/auth_success.html +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli.egg-info/dependency_links.txt +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli.egg-info/entry_points.txt +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli.egg-info/not-zip-safe +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli.egg-info/top_level.txt +0 -0
- {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudsmith-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.13.0
|
|
4
4
|
Summary: Cloudsmith Command-Line Interface (CLI)
|
|
5
5
|
Home-page: https://github.com/cloudsmith-io/cloudsmith-cli
|
|
6
6
|
Author: Cloudsmith Ltd
|
|
@@ -34,7 +34,7 @@ Requires-Dist: json5>=0.9.0
|
|
|
34
34
|
Requires-Dist: cloudsmith-api<3.0,>=2.0.24
|
|
35
35
|
Requires-Dist: keyring>=25.4.1
|
|
36
36
|
Requires-Dist: mcp==1.9.1
|
|
37
|
-
Requires-Dist: toon
|
|
37
|
+
Requires-Dist: python-toon==0.1.2
|
|
38
38
|
Requires-Dist: requests>=2.18.4
|
|
39
39
|
Requires-Dist: requests_toolbelt>=1.0.0
|
|
40
40
|
Requires-Dist: semver>=2.7.9
|
|
@@ -102,6 +102,7 @@ The CLI currently supports the following commands (and sub-commands):
|
|
|
102
102
|
- `packages`: List packages for a repository. (Aliases `repos list`)
|
|
103
103
|
- `repos`: List repositories for a namespace (owner).
|
|
104
104
|
- `login`|`token`: Retrieve your API authentication token/key via login.
|
|
105
|
+
- `logout`: Clear stored authentication credentials and SSO tokens (Keyring, API key from credential file and emit warning when `$CLOUDSMITH_API_KEY` is still set).
|
|
105
106
|
- `metrics`: Metrics and statistics for a repository.
|
|
106
107
|
- `tokens`: Retrieve bandwidth usage for entitlement tokens.
|
|
107
108
|
- `packages`: Retrieve package usage for repository.
|
|
@@ -47,6 +47,7 @@ The CLI currently supports the following commands (and sub-commands):
|
|
|
47
47
|
- `packages`: List packages for a repository. (Aliases `repos list`)
|
|
48
48
|
- `repos`: List repositories for a namespace (owner).
|
|
49
49
|
- `login`|`token`: Retrieve your API authentication token/key via login.
|
|
50
|
+
- `logout`: Clear stored authentication credentials and SSO tokens (Keyring, API key from credential file and emit warning when `$CLOUDSMITH_API_KEY` is still set).
|
|
50
51
|
- `metrics`: Metrics and statistics for a repository.
|
|
51
52
|
- `tokens`: Retrieve bandwidth usage for entitlement tokens.
|
|
52
53
|
- `packages`: Retrieve package usage for repository.
|
|
@@ -9,14 +9,16 @@ from ..exceptions import handle_api_exceptions
|
|
|
9
9
|
from ..saml import create_configured_session, get_idp_url
|
|
10
10
|
from ..webserver import AuthenticationWebRequestHandler, AuthenticationWebServer
|
|
11
11
|
from .main import main
|
|
12
|
-
from .tokens import create
|
|
12
|
+
from .tokens import create, request_api_key
|
|
13
13
|
|
|
14
14
|
# Authentication server configuration
|
|
15
15
|
AUTH_SERVER_HOST = "127.0.0.1"
|
|
16
16
|
AUTH_SERVER_PORT = 12400
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
def _perform_saml_authentication(
|
|
19
|
+
def _perform_saml_authentication(
|
|
20
|
+
opts, owner, enable_token_creation=False, use_stderr=False
|
|
21
|
+
):
|
|
20
22
|
"""Perform SAML authentication via web browser and local web server."""
|
|
21
23
|
session = create_configured_session(opts)
|
|
22
24
|
api_host = opts.api_config.host
|
|
@@ -25,12 +27,12 @@ def _perform_saml_authentication(opts, owner, enable_token_creation=False, json=
|
|
|
25
27
|
|
|
26
28
|
click.echo(
|
|
27
29
|
f"Opening your organization's SAML IDP URL in your browser: {click.style(idp_url, bold=True)}",
|
|
28
|
-
err=
|
|
30
|
+
err=use_stderr,
|
|
29
31
|
)
|
|
30
|
-
click.echo(err=
|
|
32
|
+
click.echo(err=use_stderr)
|
|
31
33
|
webbrowser.open(idp_url)
|
|
32
34
|
|
|
33
|
-
click.echo("Starting webserver to begin authentication ... ", err=
|
|
35
|
+
click.echo("Starting webserver to begin authentication ... ", err=use_stderr)
|
|
34
36
|
|
|
35
37
|
auth_server = AuthenticationWebServer(
|
|
36
38
|
(AUTH_SERVER_HOST, AUTH_SERVER_PORT),
|
|
@@ -60,14 +62,14 @@ def _perform_saml_authentication(opts, owner, enable_token_creation=False, json=
|
|
|
60
62
|
"--token",
|
|
61
63
|
default=False,
|
|
62
64
|
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.",
|
|
64
66
|
)
|
|
65
67
|
@click.option(
|
|
66
68
|
"-f",
|
|
67
69
|
"--force",
|
|
68
70
|
default=False,
|
|
69
71
|
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.",
|
|
71
73
|
)
|
|
72
74
|
@click.option(
|
|
73
75
|
"--save-config",
|
|
@@ -79,17 +81,49 @@ def _perform_saml_authentication(opts, owner, enable_token_creation=False, json=
|
|
|
79
81
|
"--json",
|
|
80
82
|
default=False,
|
|
81
83
|
is_flag=True,
|
|
82
|
-
help="Output token details in
|
|
84
|
+
help="[DEPRECATED: Use --output-format json] Output token details in JSON format.",
|
|
85
|
+
)
|
|
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.",
|
|
83
93
|
)
|
|
84
94
|
@decorators.common_cli_config_options
|
|
85
95
|
@decorators.common_cli_output_options
|
|
86
96
|
@decorators.initialise_api
|
|
87
97
|
@click.pass_context
|
|
88
|
-
def authenticate(
|
|
98
|
+
def authenticate(
|
|
99
|
+
ctx, opts, owner, token, force, save_config, json, request_api_key_flag
|
|
100
|
+
):
|
|
89
101
|
"""Authenticate to Cloudsmith using the org's SAML setup."""
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
+
)
|
|
93
127
|
|
|
94
128
|
if json and not utils.should_use_stderr(opts):
|
|
95
129
|
click.secho(
|
|
@@ -106,11 +140,34 @@ def authenticate(ctx, opts, owner, token, force, save_config, json):
|
|
|
106
140
|
err=use_stderr,
|
|
107
141
|
)
|
|
108
142
|
|
|
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
|
+
|
|
109
146
|
context_message = "Failed to authenticate via SSO!"
|
|
110
147
|
with handle_api_exceptions(ctx, opts=opts, context_msg=context_message):
|
|
111
148
|
_perform_saml_authentication(
|
|
112
|
-
opts,
|
|
149
|
+
opts,
|
|
150
|
+
owner,
|
|
151
|
+
enable_token_creation=enable_token_creation,
|
|
152
|
+
use_stderr=use_stderr,
|
|
113
153
|
)
|
|
114
154
|
|
|
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
|
+
|
|
115
172
|
if token:
|
|
116
173
|
ctx.invoke(create, opts=opts, save_config=save_config, force=force, json=json)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Copyright 2026 Cloudsmith Ltd
|
|
2
|
+
"""CLI/Commands - Log out and clear authentication state."""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
import cloudsmith_api
|
|
8
|
+
|
|
9
|
+
from ...core import keyring
|
|
10
|
+
from .. import decorators, utils
|
|
11
|
+
from ..config import CredentialsReader
|
|
12
|
+
from .main import main
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _clear_credentials(dry_run, use_stderr):
|
|
16
|
+
"""Clear credential files. Returns result dict."""
|
|
17
|
+
creds_files = CredentialsReader.find_existing_files()
|
|
18
|
+
if not creds_files:
|
|
19
|
+
click.echo("No credentials file found.", err=use_stderr)
|
|
20
|
+
return {"action": "not_found", "files": []}
|
|
21
|
+
|
|
22
|
+
if not dry_run:
|
|
23
|
+
for path in creds_files:
|
|
24
|
+
CredentialsReader.clear_api_key(path)
|
|
25
|
+
|
|
26
|
+
verb = "Would remove" if dry_run else "Removed"
|
|
27
|
+
for path in creds_files:
|
|
28
|
+
click.echo(
|
|
29
|
+
f"{verb} credentials from: " + click.style(path, bold=True),
|
|
30
|
+
err=use_stderr,
|
|
31
|
+
)
|
|
32
|
+
action = "would_remove" if dry_run else "removed"
|
|
33
|
+
return {"action": action, "files": list(creds_files)}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _clear_keyring(api_host, dry_run, use_stderr):
|
|
37
|
+
"""Clear SSO tokens from keyring. Returns result dict."""
|
|
38
|
+
if not keyring.should_use_keyring():
|
|
39
|
+
click.secho(
|
|
40
|
+
"Keyring is disabled (CLOUDSMITH_NO_KEYRING is set).",
|
|
41
|
+
fg="yellow",
|
|
42
|
+
err=use_stderr,
|
|
43
|
+
)
|
|
44
|
+
return {"action": "disabled"}
|
|
45
|
+
|
|
46
|
+
if not keyring.has_sso_tokens(api_host):
|
|
47
|
+
click.echo("No SSO tokens found in system keyring.", err=use_stderr)
|
|
48
|
+
return {"action": "not_found"}
|
|
49
|
+
|
|
50
|
+
if dry_run:
|
|
51
|
+
click.echo("Would remove SSO tokens from system keyring.", err=use_stderr)
|
|
52
|
+
return {"action": "would_remove"}
|
|
53
|
+
|
|
54
|
+
deleted = keyring.delete_sso_tokens(api_host)
|
|
55
|
+
action = "removed" if deleted else "failed"
|
|
56
|
+
msg = f"{'Removed' if deleted else 'Failed to remove'} SSO tokens from system keyring."
|
|
57
|
+
click.secho(msg, fg=None if deleted else "red", err=use_stderr)
|
|
58
|
+
return {"action": action} if deleted else {"action": action, "message": msg}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _env_api_key_status():
|
|
62
|
+
"""Return structured status for the CLOUDSMITH_API_KEY env var."""
|
|
63
|
+
is_set = bool(os.environ.get("CLOUDSMITH_API_KEY"))
|
|
64
|
+
return {
|
|
65
|
+
"is_set": is_set,
|
|
66
|
+
"action": "unset CLOUDSMITH_API_KEY" if is_set else "none",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _collect_warnings(keyring_only, config_only):
|
|
71
|
+
"""Collect advisory warnings based on flags and environment."""
|
|
72
|
+
warnings = []
|
|
73
|
+
if config_only:
|
|
74
|
+
warnings.append("SSO tokens were not modified (--config-only).")
|
|
75
|
+
if keyring_only:
|
|
76
|
+
warnings.append("credentials.ini was not modified (--keyring-only).")
|
|
77
|
+
if os.environ.get("CLOUDSMITH_API_KEY"):
|
|
78
|
+
warnings.append(
|
|
79
|
+
"CLOUDSMITH_API_KEY is set in your environment. "
|
|
80
|
+
"Run: unset CLOUDSMITH_API_KEY"
|
|
81
|
+
)
|
|
82
|
+
return warnings
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@main.command()
|
|
86
|
+
@click.option(
|
|
87
|
+
"--api-host",
|
|
88
|
+
envvar="CLOUDSMITH_API_HOST",
|
|
89
|
+
default=None,
|
|
90
|
+
help="The API host to clear keyring tokens for.",
|
|
91
|
+
)
|
|
92
|
+
@click.option(
|
|
93
|
+
"--keyring-only",
|
|
94
|
+
is_flag=True,
|
|
95
|
+
default=False,
|
|
96
|
+
help="Only clear SSO tokens from the system keyring.",
|
|
97
|
+
)
|
|
98
|
+
@click.option(
|
|
99
|
+
"--config-only",
|
|
100
|
+
is_flag=True,
|
|
101
|
+
default=False,
|
|
102
|
+
help="Only clear credentials from credentials.ini.",
|
|
103
|
+
)
|
|
104
|
+
@click.option(
|
|
105
|
+
"--dry-run",
|
|
106
|
+
is_flag=True,
|
|
107
|
+
default=False,
|
|
108
|
+
help="Show what would be removed without removing anything.",
|
|
109
|
+
)
|
|
110
|
+
@decorators.common_cli_config_options
|
|
111
|
+
@decorators.common_cli_output_options
|
|
112
|
+
@click.pass_context
|
|
113
|
+
def logout(ctx, opts, api_host, keyring_only, config_only, dry_run):
|
|
114
|
+
"""Clear stored authentication credentials and SSO tokens."""
|
|
115
|
+
if keyring_only and config_only:
|
|
116
|
+
raise click.UsageError(
|
|
117
|
+
"--keyring-only and --config-only are mutually exclusive."
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if api_host is None:
|
|
121
|
+
api_host = opts.api_host or cloudsmith_api.Configuration().host
|
|
122
|
+
|
|
123
|
+
use_stderr = utils.should_use_stderr(opts)
|
|
124
|
+
|
|
125
|
+
credential_file = (
|
|
126
|
+
_clear_credentials(dry_run, use_stderr)
|
|
127
|
+
if not keyring_only
|
|
128
|
+
else {"action": "skipped", "files": []}
|
|
129
|
+
)
|
|
130
|
+
keyring_result = (
|
|
131
|
+
_clear_keyring(api_host, dry_run, use_stderr)
|
|
132
|
+
if not config_only
|
|
133
|
+
else {"action": "skipped"}
|
|
134
|
+
)
|
|
135
|
+
warnings = _collect_warnings(keyring_only, config_only)
|
|
136
|
+
|
|
137
|
+
for warning in warnings:
|
|
138
|
+
click.secho(f"Note: {warning}", fg="yellow", err=use_stderr)
|
|
139
|
+
|
|
140
|
+
utils.maybe_print_as_json(
|
|
141
|
+
opts,
|
|
142
|
+
{
|
|
143
|
+
"api_host": api_host,
|
|
144
|
+
"dry_run": dry_run,
|
|
145
|
+
"sources": {
|
|
146
|
+
"credential_file": credential_file,
|
|
147
|
+
"keyring": keyring_result,
|
|
148
|
+
"environment_api_key": _env_api_key_status(),
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
)
|
|
@@ -31,6 +31,77 @@ def handle_duplicate_token_error(exc, ctx, opts, save_config, force, json):
|
|
|
31
31
|
raise exc
|
|
32
32
|
|
|
33
33
|
|
|
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 exc
|
|
103
|
+
|
|
104
|
+
|
|
34
105
|
@main.group(cls=command.AliasGroup, name="tokens")
|
|
35
106
|
@decorators.common_cli_config_options
|
|
36
107
|
@decorators.common_cli_output_options
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""CLI/Commands - Retrieve authentication status."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from ...core import keyring
|
|
8
|
+
from ...core.api.exceptions import ApiException
|
|
9
|
+
from ...core.api.user import get_token_metadata, get_user_brief
|
|
10
|
+
from .. import decorators, utils
|
|
11
|
+
from ..config import CredentialsReader
|
|
12
|
+
from ..exceptions import handle_api_exceptions
|
|
13
|
+
from .main import main
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _get_active_method(api_config):
|
|
17
|
+
"""Inspect API config to determine SSO, API key, or no auth."""
|
|
18
|
+
headers = getattr(api_config, "headers", {}) or {}
|
|
19
|
+
if headers.get("Authorization", "").startswith("Bearer "):
|
|
20
|
+
return "sso_token"
|
|
21
|
+
if (getattr(api_config, "api_key", {}) or {}).get("X-Api-Key"):
|
|
22
|
+
return "api_key"
|
|
23
|
+
return "none"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _get_api_key_source(opts):
|
|
27
|
+
"""Determine where the API key was loaded from.
|
|
28
|
+
|
|
29
|
+
Checks in priority order matching actual resolution:
|
|
30
|
+
CLI --api-key flag > CLOUDSMITH_API_KEY env var > credentials.ini.
|
|
31
|
+
"""
|
|
32
|
+
if not opts.api_key:
|
|
33
|
+
return {"configured": False, "source": None, "source_key": None}
|
|
34
|
+
|
|
35
|
+
env_key = os.environ.get("CLOUDSMITH_API_KEY")
|
|
36
|
+
|
|
37
|
+
# If env var is set but differs from the resolved key, CLI flag won
|
|
38
|
+
if env_key and opts.api_key != env_key:
|
|
39
|
+
source, key = "CLI --api-key flag", "cli_flag"
|
|
40
|
+
elif env_key:
|
|
41
|
+
suffix = env_key[-4:]
|
|
42
|
+
source, key = f"CLOUDSMITH_API_KEY env var (ends with ...{suffix})", "env_var"
|
|
43
|
+
elif creds := CredentialsReader.find_existing_files():
|
|
44
|
+
source, key = f"credentials.ini ({creds[0]})", "credentials_file"
|
|
45
|
+
else:
|
|
46
|
+
source, key = "CLI --api-key flag", "cli_flag"
|
|
47
|
+
|
|
48
|
+
return {"configured": True, "source": source, "source_key": key}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _get_sso_status(api_host):
|
|
52
|
+
"""Return SSO token status from the system keyring."""
|
|
53
|
+
enabled = keyring.should_use_keyring()
|
|
54
|
+
has_tokens = enabled and keyring.has_sso_tokens(api_host)
|
|
55
|
+
refreshed = keyring.get_refresh_attempted_at(api_host) if has_tokens else None
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
"configured": has_tokens,
|
|
59
|
+
"keyring_enabled": enabled,
|
|
60
|
+
"source": "System Keyring" if has_tokens else None,
|
|
61
|
+
"last_refreshed": utils.fmt_datetime(refreshed) if refreshed else None,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _get_verbose_auth_data(opts, api_host):
|
|
66
|
+
"""Gather all auth details for verbose output."""
|
|
67
|
+
api_key_info = _get_api_key_source(opts)
|
|
68
|
+
sso_info = _get_sso_status(api_host)
|
|
69
|
+
|
|
70
|
+
# Fetch token metadata (extra API call, graceful fallback)
|
|
71
|
+
token_meta = None
|
|
72
|
+
if api_key_info["configured"]:
|
|
73
|
+
try:
|
|
74
|
+
token_meta = get_token_metadata()
|
|
75
|
+
except ApiException:
|
|
76
|
+
token_meta = None
|
|
77
|
+
|
|
78
|
+
created = token_meta.get("created") if token_meta else None
|
|
79
|
+
api_key_info["slug"] = token_meta["slug"] if token_meta else None
|
|
80
|
+
api_key_info["created"] = utils.fmt_datetime(created) if created else None
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
"active_method": _get_active_method(opts.api_config),
|
|
84
|
+
"api_key": api_key_info,
|
|
85
|
+
"sso": sso_info,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _print_user_line(name, username, email):
|
|
90
|
+
"""Print a styled user identity line."""
|
|
91
|
+
styled_name = click.style(name or "Unknown", fg="cyan")
|
|
92
|
+
styled_slug = click.style(username or "Unknown", fg="magenta")
|
|
93
|
+
email_part = f", email: {click.style(email, fg='green')}" if email else ""
|
|
94
|
+
click.echo(f"User: {styled_name} (slug: {styled_slug}{email_part})")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _print_verbose_text(data):
|
|
98
|
+
"""Print verbose authentication details as styled text."""
|
|
99
|
+
click.echo()
|
|
100
|
+
_print_user_line(data["name"], data["username"], data.get("email"))
|
|
101
|
+
|
|
102
|
+
auth = data["auth"]
|
|
103
|
+
active = auth["active_method"]
|
|
104
|
+
ak = auth["api_key"]
|
|
105
|
+
sso = auth["sso"]
|
|
106
|
+
|
|
107
|
+
click.echo()
|
|
108
|
+
if active == "sso_token":
|
|
109
|
+
click.secho("Authentication Method: SSO Token (primary)", fg="cyan", bold=True)
|
|
110
|
+
if sso.get("source"):
|
|
111
|
+
click.echo(f" Source: {sso['source']}")
|
|
112
|
+
if sso.get("last_refreshed"):
|
|
113
|
+
click.echo(
|
|
114
|
+
f" Last Refreshed: {sso['last_refreshed']} (refreshes every 30 min)"
|
|
115
|
+
)
|
|
116
|
+
if ak["configured"]:
|
|
117
|
+
click.echo()
|
|
118
|
+
click.secho("API Key: Also configured", fg="yellow")
|
|
119
|
+
if ak.get("source"):
|
|
120
|
+
click.echo(f" Source: {ak['source']}")
|
|
121
|
+
click.echo(" Note: SSO token is being used instead")
|
|
122
|
+
elif active == "api_key":
|
|
123
|
+
click.secho("Authentication Method: API Key", fg="cyan", bold=True)
|
|
124
|
+
for label, field in [
|
|
125
|
+
("Source", "source"),
|
|
126
|
+
("Token Slug", "slug"),
|
|
127
|
+
("Created", "created"),
|
|
128
|
+
]:
|
|
129
|
+
if ak.get(field):
|
|
130
|
+
click.echo(f" {label}: {ak[field]}")
|
|
131
|
+
else:
|
|
132
|
+
click.secho("Authentication Method: None (anonymous)", fg="yellow", bold=True)
|
|
133
|
+
|
|
134
|
+
if active != "sso_token":
|
|
135
|
+
click.echo()
|
|
136
|
+
if not sso["keyring_enabled"]:
|
|
137
|
+
click.secho(
|
|
138
|
+
"SSO Status: Keyring disabled (CLOUDSMITH_NO_KEYRING)", fg="yellow"
|
|
139
|
+
)
|
|
140
|
+
elif sso["configured"]:
|
|
141
|
+
click.secho("SSO Status: Configured (not active)", fg="yellow")
|
|
142
|
+
click.echo(f" Source: {sso['source']}")
|
|
143
|
+
else:
|
|
144
|
+
click.echo("SSO Status: Not configured")
|
|
145
|
+
click.echo(" Keyring: Enabled (no tokens stored)")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@main.command()
|
|
149
|
+
@decorators.common_cli_config_options
|
|
150
|
+
@decorators.common_cli_output_options
|
|
151
|
+
@decorators.common_api_auth_options
|
|
152
|
+
@decorators.initialise_api
|
|
153
|
+
@click.pass_context
|
|
154
|
+
def whoami(ctx, opts):
|
|
155
|
+
"""Retrieve your current authentication status."""
|
|
156
|
+
use_stderr = utils.should_use_stderr(opts)
|
|
157
|
+
|
|
158
|
+
click.echo(
|
|
159
|
+
"Retrieving your authentication status from the API ... ",
|
|
160
|
+
nl=False,
|
|
161
|
+
err=use_stderr,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
context_msg = "Failed to retrieve your authentication status!"
|
|
165
|
+
with handle_api_exceptions(ctx, opts=opts, context_msg=context_msg):
|
|
166
|
+
with utils.maybe_spinner(opts):
|
|
167
|
+
is_auth, username, email, name = get_user_brief()
|
|
168
|
+
click.secho("OK", fg="green", err=use_stderr)
|
|
169
|
+
|
|
170
|
+
data = {
|
|
171
|
+
"is_authenticated": is_auth,
|
|
172
|
+
"username": username,
|
|
173
|
+
"email": email,
|
|
174
|
+
"name": name,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if opts.verbose:
|
|
178
|
+
api_host = getattr(opts.api_config, "host", None) or opts.api_host
|
|
179
|
+
data["auth"] = _get_verbose_auth_data(opts, api_host)
|
|
180
|
+
|
|
181
|
+
if utils.maybe_print_as_json(opts, data):
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
if not is_auth:
|
|
185
|
+
click.echo("You are authenticated as:")
|
|
186
|
+
click.secho("Nobody (i.e. anonymous user)", fg="yellow")
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
if opts.verbose:
|
|
190
|
+
_print_verbose_text(data)
|
|
191
|
+
else:
|
|
192
|
+
click.echo("You are authenticated as:")
|
|
193
|
+
_print_user_line(name, username, email)
|
|
@@ -208,6 +208,44 @@ class CredentialsReader(ConfigReader):
|
|
|
208
208
|
config_searchpath = list(_CFG_SEARCH_PATHS)
|
|
209
209
|
config_section_schemas = [CredentialsSchema.Default, CredentialsSchema.Profile]
|
|
210
210
|
|
|
211
|
+
@classmethod
|
|
212
|
+
def find_existing_files(cls):
|
|
213
|
+
"""Return a list of existing credentials file paths."""
|
|
214
|
+
paths = []
|
|
215
|
+
seen = set()
|
|
216
|
+
for filename in cls.config_files:
|
|
217
|
+
for searchpath in cls.config_searchpath:
|
|
218
|
+
path = os.path.join(searchpath, filename)
|
|
219
|
+
if os.path.exists(path) and path not in seen:
|
|
220
|
+
paths.append(path)
|
|
221
|
+
seen.add(path)
|
|
222
|
+
return paths
|
|
223
|
+
|
|
224
|
+
@classmethod
|
|
225
|
+
def _set_api_key(cls, path, api_key=""):
|
|
226
|
+
"""Write api_key value in a credentials file, preserving structure."""
|
|
227
|
+
with open(path) as f:
|
|
228
|
+
content = f.read()
|
|
229
|
+
replacement = rf"\1 = {api_key}" if api_key else r"\1 ="
|
|
230
|
+
content = re.sub(
|
|
231
|
+
r"^(api_key)\s*=\s*.*$",
|
|
232
|
+
replacement,
|
|
233
|
+
content,
|
|
234
|
+
flags=re.MULTILINE,
|
|
235
|
+
)
|
|
236
|
+
with open(path, "w") as f:
|
|
237
|
+
f.write(content)
|
|
238
|
+
|
|
239
|
+
@classmethod
|
|
240
|
+
def clear_api_key(cls, path):
|
|
241
|
+
"""Clear api_key values in a credentials file, preserving structure."""
|
|
242
|
+
cls._set_api_key(path)
|
|
243
|
+
|
|
244
|
+
@classmethod
|
|
245
|
+
def update_api_key(cls, path, api_key):
|
|
246
|
+
"""Update api_key value in an existing credentials file, preserving structure."""
|
|
247
|
+
cls._set_api_key(path, api_key)
|
|
248
|
+
|
|
211
249
|
|
|
212
250
|
class Options:
|
|
213
251
|
"""Options object that holds config for the application."""
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MockToken:
|
|
5
|
+
"""Mock Token object with the properties needed for testing."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, key, created, slug_perm):
|
|
8
|
+
self.key = key
|
|
9
|
+
self.created = created
|
|
10
|
+
self.slug_perm = slug_perm
|
|
11
|
+
|
|
12
|
+
def to_dict(self):
|
|
13
|
+
return {
|
|
14
|
+
"key": self.key,
|
|
15
|
+
"created": self.created,
|
|
16
|
+
"slug_perm": self.slug_perm,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture
|
|
21
|
+
def mock_token():
|
|
22
|
+
"""Return a default MockToken for use in tests."""
|
|
23
|
+
return MockToken(
|
|
24
|
+
key="ck_test123456",
|
|
25
|
+
created="2026-02-06T00:00:00Z",
|
|
26
|
+
slug_perm="test-token",
|
|
27
|
+
)
|