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.
Files changed (132) hide show
  1. {cloudsmith_cli-1.12.1/cloudsmith_cli.egg-info → cloudsmith_cli-1.13.0}/PKG-INFO +3 -2
  2. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/README.md +1 -0
  3. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/__init__.py +1 -0
  4. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/auth.py +70 -13
  5. cloudsmith_cli-1.13.0/cloudsmith_cli/cli/commands/logout.py +151 -0
  6. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/tokens.py +71 -0
  7. cloudsmith_cli-1.13.0/cloudsmith_cli/cli/commands/whoami.py +193 -0
  8. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/config.py +38 -0
  9. cloudsmith_cli-1.13.0/cloudsmith_cli/cli/tests/commands/conftest.py +27 -0
  10. cloudsmith_cli-1.13.0/cloudsmith_cli/cli/tests/commands/test_auth.py +283 -0
  11. cloudsmith_cli-1.13.0/cloudsmith_cli/cli/tests/commands/test_logout.py +147 -0
  12. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/test_tokens.py +75 -8
  13. cloudsmith_cli-1.13.0/cloudsmith_cli/cli/tests/test_webserver.py +155 -0
  14. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/webserver.py +23 -10
  15. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/init.py +34 -16
  16. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/user.py +10 -0
  17. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/config.py +22 -0
  18. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/download.py +1 -1
  19. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/keyring.py +51 -0
  20. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/mcp/server.py +4 -10
  21. cloudsmith_cli-1.13.0/cloudsmith_cli/core/tests/test_init.py +371 -0
  22. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/tests/test_keyring.py +99 -5
  23. cloudsmith_cli-1.13.0/cloudsmith_cli/data/VERSION +1 -0
  24. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0/cloudsmith_cli.egg-info}/PKG-INFO +3 -2
  25. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli.egg-info/SOURCES.txt +5 -0
  26. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli.egg-info/requires.txt +1 -1
  27. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/setup.py +1 -1
  28. cloudsmith_cli-1.12.1/cloudsmith_cli/cli/commands/whoami.py +0 -62
  29. cloudsmith_cli-1.12.1/cloudsmith_cli/core/tests/test_init.py +0 -186
  30. cloudsmith_cli-1.12.1/cloudsmith_cli/data/VERSION +0 -1
  31. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/LICENSE +0 -0
  32. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/MANIFEST.in +0 -0
  33. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/__init__.py +0 -0
  34. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/__main__.py +0 -0
  35. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/__init__.py +0 -0
  36. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/command.py +0 -0
  37. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/check.py +0 -0
  38. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/copy.py +0 -0
  39. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/delete.py +0 -0
  40. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/dependencies.py +0 -0
  41. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/docs.py +0 -0
  42. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/download.py +0 -0
  43. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/entitlements.py +0 -0
  44. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/help_.py +0 -0
  45. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/list_.py +0 -0
  46. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/login.py +0 -0
  47. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/main.py +0 -0
  48. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/mcp.py +0 -0
  49. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/metrics/__init__.py +0 -0
  50. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/metrics/command.py +0 -0
  51. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/metrics/entitlements.py +0 -0
  52. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/metrics/packages.py +0 -0
  53. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/move.py +0 -0
  54. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/policy/__init__.py +0 -0
  55. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/policy/command.py +0 -0
  56. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/policy/deny.py +0 -0
  57. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/policy/license.py +0 -0
  58. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/policy/vulnerability.py +0 -0
  59. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/push.py +0 -0
  60. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/quarantine.py +0 -0
  61. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/quota/__init__.py +0 -0
  62. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/quota/command.py +0 -0
  63. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/quota/history.py +0 -0
  64. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/quota/quota.py +0 -0
  65. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/repos.py +0 -0
  66. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/resync.py +0 -0
  67. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/status.py +0 -0
  68. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/tags.py +0 -0
  69. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/commands/upstream.py +0 -0
  70. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/decorators.py +0 -0
  71. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/exceptions.py +0 -0
  72. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/saml.py +0 -0
  73. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/table.py +0 -0
  74. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/__init__.py +0 -0
  75. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/__init__.py +0 -0
  76. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/policy/__init__.py +0 -0
  77. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/policy/test_deny.py +0 -0
  78. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/policy/test_licence.py +0 -0
  79. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/policy/test_vulnerability.py +0 -0
  80. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/test_check.py +0 -0
  81. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/test_download.py +0 -0
  82. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/test_entitlements.py +0 -0
  83. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/test_login.py +0 -0
  84. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/test_main.py +0 -0
  85. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/test_mcp.py +0 -0
  86. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/test_package_commands.py +0 -0
  87. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/test_repos.py +0 -0
  88. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/commands/test_upstream.py +0 -0
  89. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/conftest.py +0 -0
  90. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/test_push.py +0 -0
  91. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/test_saml.py +0 -0
  92. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/test_utils.py +0 -0
  93. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/tests/utils.py +0 -0
  94. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/types.py +0 -0
  95. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/utils.py +0 -0
  96. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/cli/validators.py +0 -0
  97. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/__init__.py +0 -0
  98. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/__init__.py +0 -0
  99. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/distros.py +0 -0
  100. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/entitlements.py +0 -0
  101. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/exceptions.py +0 -0
  102. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/files.py +0 -0
  103. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/metrics.py +0 -0
  104. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/orgs.py +0 -0
  105. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/packages.py +0 -0
  106. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/quota.py +0 -0
  107. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/rates.py +0 -0
  108. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/repos.py +0 -0
  109. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/status.py +0 -0
  110. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/upstreams.py +0 -0
  111. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/api/version.py +0 -0
  112. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/mcp/__init__.py +0 -0
  113. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/mcp/data.py +0 -0
  114. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/pagination.py +0 -0
  115. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/ratelimits.py +0 -0
  116. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/rest.py +0 -0
  117. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/tests/__init__.py +0 -0
  118. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/tests/test_download.py +0 -0
  119. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/tests/test_rest.py +0 -0
  120. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/tests/test_version.py +0 -0
  121. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/utils.py +0 -0
  122. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/core/version.py +0 -0
  123. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/data/config.ini +0 -0
  124. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/data/credentials.ini +0 -0
  125. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/templates/__init__.py +0 -0
  126. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/templates/auth_error.html +0 -0
  127. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli/templates/auth_success.html +0 -0
  128. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli.egg-info/dependency_links.txt +0 -0
  129. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli.egg-info/entry_points.txt +0 -0
  130. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli.egg-info/not-zip-safe +0 -0
  131. {cloudsmith_cli-1.12.1 → cloudsmith_cli-1.13.0}/cloudsmith_cli.egg-info/top_level.txt +0 -0
  132. {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.12.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-python==0.1.2
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.
@@ -12,6 +12,7 @@ from . import (
12
12
  help_,
13
13
  list_,
14
14
  login,
15
+ logout,
15
16
  mcp,
16
17
  metrics,
17
18
  move,
@@ -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(opts, owner, enable_token_creation=False, json=False):
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=json,
30
+ err=use_stderr,
29
31
  )
30
- click.echo(err=json)
32
+ click.echo(err=use_stderr)
31
33
  webbrowser.open(idp_url)
32
34
 
33
- click.echo("Starting webserver to begin authentication ... ", err=json)
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 json format.",
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(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
+ ):
89
101
  """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
+ )
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, owner, enable_token_creation=token, json=json
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
+ )