cloudsmith-cli 1.20.0__py3-none-any.whl

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 (130) hide show
  1. cloudsmith_cli/__init__.py +10 -0
  2. cloudsmith_cli/__main__.py +8 -0
  3. cloudsmith_cli/cli/__init__.py +1 -0
  4. cloudsmith_cli/cli/command.py +160 -0
  5. cloudsmith_cli/cli/commands/__init__.py +33 -0
  6. cloudsmith_cli/cli/commands/auth.py +173 -0
  7. cloudsmith_cli/cli/commands/check.py +129 -0
  8. cloudsmith_cli/cli/commands/copy.py +98 -0
  9. cloudsmith_cli/cli/commands/credential_helper/__init__.py +39 -0
  10. cloudsmith_cli/cli/commands/credential_helper/docker.py +66 -0
  11. cloudsmith_cli/cli/commands/credential_helper/manage.py +299 -0
  12. cloudsmith_cli/cli/commands/delete.py +67 -0
  13. cloudsmith_cli/cli/commands/dependencies.py +108 -0
  14. cloudsmith_cli/cli/commands/docs.py +16 -0
  15. cloudsmith_cli/cli/commands/download.py +620 -0
  16. cloudsmith_cli/cli/commands/entitlements.py +819 -0
  17. cloudsmith_cli/cli/commands/help_.py +12 -0
  18. cloudsmith_cli/cli/commands/list_.py +317 -0
  19. cloudsmith_cli/cli/commands/login.py +99 -0
  20. cloudsmith_cli/cli/commands/logout.py +151 -0
  21. cloudsmith_cli/cli/commands/main.py +73 -0
  22. cloudsmith_cli/cli/commands/mcp.py +523 -0
  23. cloudsmith_cli/cli/commands/metadata.py +503 -0
  24. cloudsmith_cli/cli/commands/metrics/__init__.py +2 -0
  25. cloudsmith_cli/cli/commands/metrics/command.py +20 -0
  26. cloudsmith_cli/cli/commands/metrics/entitlements.py +148 -0
  27. cloudsmith_cli/cli/commands/metrics/packages.py +134 -0
  28. cloudsmith_cli/cli/commands/move.py +111 -0
  29. cloudsmith_cli/cli/commands/policy/__init__.py +3 -0
  30. cloudsmith_cli/cli/commands/policy/command.py +20 -0
  31. cloudsmith_cli/cli/commands/policy/deny.py +248 -0
  32. cloudsmith_cli/cli/commands/policy/license.py +335 -0
  33. cloudsmith_cli/cli/commands/policy/vulnerability.py +322 -0
  34. cloudsmith_cli/cli/commands/push.py +1323 -0
  35. cloudsmith_cli/cli/commands/quarantine.py +148 -0
  36. cloudsmith_cli/cli/commands/quota/__init__.py +2 -0
  37. cloudsmith_cli/cli/commands/quota/command.py +20 -0
  38. cloudsmith_cli/cli/commands/quota/history.py +122 -0
  39. cloudsmith_cli/cli/commands/quota/quota.py +107 -0
  40. cloudsmith_cli/cli/commands/repos.py +330 -0
  41. cloudsmith_cli/cli/commands/resync.py +90 -0
  42. cloudsmith_cli/cli/commands/status.py +92 -0
  43. cloudsmith_cli/cli/commands/tags.py +375 -0
  44. cloudsmith_cli/cli/commands/tokens.py +318 -0
  45. cloudsmith_cli/cli/commands/upstream.py +479 -0
  46. cloudsmith_cli/cli/commands/vulnerabilities.py +141 -0
  47. cloudsmith_cli/cli/commands/whoami.py +188 -0
  48. cloudsmith_cli/cli/config.py +635 -0
  49. cloudsmith_cli/cli/decorators.py +624 -0
  50. cloudsmith_cli/cli/exceptions.py +215 -0
  51. cloudsmith_cli/cli/metadata_common.py +146 -0
  52. cloudsmith_cli/cli/saml.py +109 -0
  53. cloudsmith_cli/cli/table.py +59 -0
  54. cloudsmith_cli/cli/types.py +14 -0
  55. cloudsmith_cli/cli/utils.py +267 -0
  56. cloudsmith_cli/cli/validators.py +378 -0
  57. cloudsmith_cli/cli/webserver.py +263 -0
  58. cloudsmith_cli/core/__init__.py +1 -0
  59. cloudsmith_cli/core/api/__init__.py +1 -0
  60. cloudsmith_cli/core/api/distros.py +31 -0
  61. cloudsmith_cli/core/api/entitlements.py +130 -0
  62. cloudsmith_cli/core/api/exceptions.py +57 -0
  63. cloudsmith_cli/core/api/files.py +131 -0
  64. cloudsmith_cli/core/api/init.py +109 -0
  65. cloudsmith_cli/core/api/metadata.py +217 -0
  66. cloudsmith_cli/core/api/metrics.py +78 -0
  67. cloudsmith_cli/core/api/orgs.py +201 -0
  68. cloudsmith_cli/core/api/packages.py +309 -0
  69. cloudsmith_cli/core/api/quota.py +64 -0
  70. cloudsmith_cli/core/api/rates.py +28 -0
  71. cloudsmith_cli/core/api/repos.py +81 -0
  72. cloudsmith_cli/core/api/status.py +27 -0
  73. cloudsmith_cli/core/api/upstreams.py +72 -0
  74. cloudsmith_cli/core/api/user.py +109 -0
  75. cloudsmith_cli/core/api/version.py +15 -0
  76. cloudsmith_cli/core/api/vulnerabilities.py +230 -0
  77. cloudsmith_cli/core/cache_utils.py +160 -0
  78. cloudsmith_cli/core/config.py +140 -0
  79. cloudsmith_cli/core/credentials/__init__.py +0 -0
  80. cloudsmith_cli/core/credentials/chain.py +69 -0
  81. cloudsmith_cli/core/credentials/models.py +44 -0
  82. cloudsmith_cli/core/credentials/oidc/__init__.py +6 -0
  83. cloudsmith_cli/core/credentials/oidc/cache.py +220 -0
  84. cloudsmith_cli/core/credentials/oidc/detectors/__init__.py +122 -0
  85. cloudsmith_cli/core/credentials/oidc/detectors/aws.py +85 -0
  86. cloudsmith_cli/core/credentials/oidc/detectors/azure_devops.py +70 -0
  87. cloudsmith_cli/core/credentials/oidc/detectors/base.py +26 -0
  88. cloudsmith_cli/core/credentials/oidc/detectors/bitbucket_pipelines.py +35 -0
  89. cloudsmith_cli/core/credentials/oidc/detectors/circleci.py +40 -0
  90. cloudsmith_cli/core/credentials/oidc/detectors/generic.py +42 -0
  91. cloudsmith_cli/core/credentials/oidc/detectors/github_actions.py +64 -0
  92. cloudsmith_cli/core/credentials/oidc/detectors/gitlab_ci.py +49 -0
  93. cloudsmith_cli/core/credentials/oidc/exchange.py +87 -0
  94. cloudsmith_cli/core/credentials/provider.py +17 -0
  95. cloudsmith_cli/core/credentials/providers/__init__.py +15 -0
  96. cloudsmith_cli/core/credentials/providers/cli_flag.py +24 -0
  97. cloudsmith_cli/core/credentials/providers/credentials_file.py +24 -0
  98. cloudsmith_cli/core/credentials/providers/env_var.py +24 -0
  99. cloudsmith_cli/core/credentials/providers/keyring_provider.py +59 -0
  100. cloudsmith_cli/core/credentials/providers/oidc_provider.py +115 -0
  101. cloudsmith_cli/core/download.py +594 -0
  102. cloudsmith_cli/core/keyring.py +171 -0
  103. cloudsmith_cli/core/mcp/__init__.py +0 -0
  104. cloudsmith_cli/core/mcp/data.py +17 -0
  105. cloudsmith_cli/core/mcp/server.py +786 -0
  106. cloudsmith_cli/core/pagination.py +131 -0
  107. cloudsmith_cli/core/ratelimits.py +88 -0
  108. cloudsmith_cli/core/rest.py +255 -0
  109. cloudsmith_cli/core/utils.py +95 -0
  110. cloudsmith_cli/core/version.py +20 -0
  111. cloudsmith_cli/credential_helpers/__init__.py +7 -0
  112. cloudsmith_cli/credential_helpers/backends.py +41 -0
  113. cloudsmith_cli/credential_helpers/common.py +111 -0
  114. cloudsmith_cli/credential_helpers/custom_domains.py +281 -0
  115. cloudsmith_cli/credential_helpers/docker/__init__.py +4 -0
  116. cloudsmith_cli/credential_helpers/docker/installer.py +349 -0
  117. cloudsmith_cli/credential_helpers/docker/runtime.py +117 -0
  118. cloudsmith_cli/credential_helpers/launchers.py +175 -0
  119. cloudsmith_cli/data/VERSION +1 -0
  120. cloudsmith_cli/data/config.ini +23 -0
  121. cloudsmith_cli/data/credentials.ini +14 -0
  122. cloudsmith_cli/templates/__init__.py +3 -0
  123. cloudsmith_cli/templates/auth_error.html +45 -0
  124. cloudsmith_cli/templates/auth_success.html +37 -0
  125. cloudsmith_cli-1.20.0.dist-info/METADATA +610 -0
  126. cloudsmith_cli-1.20.0.dist-info/RECORD +130 -0
  127. cloudsmith_cli-1.20.0.dist-info/WHEEL +5 -0
  128. cloudsmith_cli-1.20.0.dist-info/entry_points.txt +2 -0
  129. cloudsmith_cli-1.20.0.dist-info/licenses/LICENSE +201 -0
  130. cloudsmith_cli-1.20.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,624 @@
1
+ """CLI - Decorators."""
2
+
3
+ import functools
4
+ import os
5
+
6
+ import click
7
+ from click.core import ParameterSource
8
+
9
+ from cloudsmith_cli.cli import validators
10
+
11
+ from ..core.api.init import initialise_api as _initialise_api
12
+ from ..core.credentials.chain import CredentialProviderChain
13
+ from ..core.credentials.models import CredentialContext
14
+ from ..core.credentials.oidc.detectors import (
15
+ disabled_detectors_from_env,
16
+ registered_detectors,
17
+ )
18
+ from ..core.mcp import server
19
+ from ..core.rest import create_requests_session as _create_session
20
+ from . import config, utils
21
+
22
+
23
+ def report_retry(seconds, context=None):
24
+ if context == "retry-after":
25
+ click.echo()
26
+ click.echo(
27
+ "Request was throttled (429): Retrying after %(seconds)s second(s) ... "
28
+ % {"seconds": click.style(str(seconds), bold=True)}
29
+ )
30
+
31
+
32
+ def _pop_boolean_flag(kwargs, name, invert=False):
33
+ """Pop a boolean flag from kwargs, optionally inverting it."""
34
+ value = kwargs.pop(name)
35
+ if value is not None and invert:
36
+ value = not value
37
+ return value
38
+
39
+
40
+ def common_package_action_options(f):
41
+ """Add common options for package actions."""
42
+
43
+ @click.option(
44
+ "-s",
45
+ "--skip-errors",
46
+ default=False,
47
+ is_flag=True,
48
+ help="Skip/ignore errors when copying packages.",
49
+ )
50
+ @click.option(
51
+ "-W",
52
+ "--no-wait-for-sync",
53
+ default=False,
54
+ is_flag=True,
55
+ help="Don't wait for package synchronisation to complete before exiting.",
56
+ )
57
+ @click.option(
58
+ "-I",
59
+ "--wait-interval",
60
+ default=5.0,
61
+ type=float,
62
+ show_default=True,
63
+ help=(
64
+ "The minimum time in seconds to wait between checking sync status after "
65
+ "uploading. This is cumulative, so that status checks happen with less "
66
+ "frequency over time, upto a maximum of 5 minutes of waiting."
67
+ ),
68
+ )
69
+ @click.option(
70
+ "--sync-attempts",
71
+ default=3,
72
+ type=int,
73
+ help="Number of times to attempt package synchronisation. If the "
74
+ "package fails the first time, the client will attempt to "
75
+ "automatically resynchronise it.",
76
+ )
77
+ @click.pass_context
78
+ @functools.wraps(f)
79
+ def wrapper(ctx, *args, **kwargs):
80
+ # pylint: disable=missing-docstring
81
+ return ctx.invoke(f, *args, **kwargs)
82
+
83
+ return wrapper
84
+
85
+
86
+ def common_cli_config_options(f):
87
+ """Add common CLI config options to commands."""
88
+
89
+ @click.option(
90
+ "-C",
91
+ "--config-file",
92
+ envvar="CLOUDSMITH_CONFIG_FILE",
93
+ type=click.Path(dir_okay=True, exists=True, writable=False, resolve_path=True),
94
+ help="The path to your config.ini file.",
95
+ )
96
+ @click.option(
97
+ "--credentials-file",
98
+ envvar="CLOUDSMITH_CREDENTIALS_FILE",
99
+ type=click.Path(dir_okay=True, exists=True, writable=False, resolve_path=True),
100
+ help="The path to your credentials.ini file.",
101
+ )
102
+ @click.option(
103
+ "-P",
104
+ "--profile",
105
+ default=None,
106
+ envvar="CLOUDSMITH_PROFILE",
107
+ help="The name of the profile to use for configuration.",
108
+ )
109
+ @click.pass_context
110
+ @functools.wraps(f)
111
+ def wrapper(ctx, *args, **kwargs):
112
+ # pylint: disable=missing-docstring
113
+ opts = config.get_or_create_options(ctx)
114
+ profile = kwargs.pop("profile") or ctx.meta.get("profile")
115
+ config_file = kwargs.pop("config_file") or ctx.meta.get("config_file")
116
+ creds_file = kwargs.pop("credentials_file") or ctx.meta.get("creds_file")
117
+
118
+ # Store in context for subcommands to inherit
119
+ ctx.meta["profile"] = profile
120
+ ctx.meta["config_file"] = config_file
121
+ ctx.meta["creds_file"] = creds_file
122
+
123
+ opts.load_config_file(path=config_file, profile=profile)
124
+ opts.load_creds_file(path=creds_file, profile=profile)
125
+ opts.api_key_from_file = opts.api_key
126
+ kwargs["opts"] = opts
127
+ return ctx.invoke(f, *args, **kwargs)
128
+
129
+ return wrapper
130
+
131
+
132
+ def common_cli_output_options(f):
133
+ """Add common CLI output options to commands."""
134
+
135
+ @click.option(
136
+ "-d",
137
+ "--debug",
138
+ default=False,
139
+ is_flag=True,
140
+ help="Produce debug output during processing.",
141
+ )
142
+ @click.option(
143
+ "-F",
144
+ "--output-format",
145
+ default="pretty",
146
+ type=click.Choice(["pretty", "json", "pretty_json"]),
147
+ help="Determines how output is formatted. This is only supported by a "
148
+ "subset of the commands at the moment (e.g. list).",
149
+ )
150
+ @click.option(
151
+ "-v",
152
+ "--verbose",
153
+ is_flag=True,
154
+ default=False,
155
+ help="Produce more output during processing.",
156
+ )
157
+ @click.pass_context
158
+ @functools.wraps(f)
159
+ def wrapper(ctx, *args, **kwargs):
160
+ # pylint: disable=missing-docstring
161
+ opts = config.get_or_create_options(ctx)
162
+ opts.debug = kwargs.pop("debug")
163
+ opts.output = kwargs.pop("output_format")
164
+ opts.verbose = kwargs.pop("verbose")
165
+ kwargs["opts"] = opts
166
+ return ctx.invoke(f, *args, **kwargs)
167
+
168
+ return wrapper
169
+
170
+
171
+ def common_cli_list_options(f):
172
+ """Add common list options to commands."""
173
+
174
+ @click.option(
175
+ "-l",
176
+ "--page-size",
177
+ default=30,
178
+ type=validators.IntOrWildcard(),
179
+ help="The amount of items to view per page for lists. Use '*' to show all results.",
180
+ callback=validators.validate_page_size,
181
+ )
182
+ @click.option(
183
+ "-p",
184
+ "--page",
185
+ default=1,
186
+ type=int,
187
+ help="The page to view for lists, where 1 is the first page",
188
+ callback=validators.validate_page,
189
+ )
190
+ @click.option(
191
+ "--page-all",
192
+ "--show-all",
193
+ default=False,
194
+ is_flag=True,
195
+ help="Show all results. Cannot be used with --page (-p) or --page-size (-l).",
196
+ ) # Validation performed post-parse for order-independence.
197
+ @click.pass_context
198
+ @functools.wraps(f)
199
+ def wrapper(ctx, *args, **kwargs):
200
+ # pylint: disable=missing-docstring
201
+ opts = config.get_or_create_options(ctx)
202
+
203
+ # Handle wildcard page_size (converts to page_all behavior)
204
+ page_size = kwargs.get("page_size")
205
+ if page_size == -1:
206
+ kwargs["page_all"] = True
207
+ # Validate that wildcard isn't used with explicit --page
208
+ validators.enforce_page_all_exclusive(ctx, wildcard_used=True)
209
+
210
+ # Order-independent validation: perform after all options parsed.
211
+ page_all = kwargs.get("page_all")
212
+ if page_all:
213
+ validators.enforce_page_all_exclusive(ctx)
214
+ kwargs["opts"] = opts
215
+ return ctx.invoke(f, *args, **kwargs)
216
+
217
+ return wrapper
218
+
219
+
220
+ def common_api_auth_options(f):
221
+ """Add common API authentication options to commands."""
222
+
223
+ @click.option(
224
+ "-k",
225
+ "--api-key",
226
+ hide_input=True,
227
+ envvar="CLOUDSMITH_API_KEY",
228
+ help="The API key for authenticating with the API.",
229
+ )
230
+ @click.pass_context
231
+ @functools.wraps(f)
232
+ def wrapper(ctx, *args, **kwargs):
233
+ # pylint: disable=missing-docstring
234
+ opts = config.get_or_create_options(ctx)
235
+ api_key = kwargs.pop("api_key")
236
+
237
+ source = ctx.get_parameter_source("api_key")
238
+ api_key_nonempty = api_key and api_key.strip()
239
+ if source == ParameterSource.COMMANDLINE and api_key_nonempty:
240
+ opts.api_key_from_flag = api_key
241
+ opts.api_key_from_env = None
242
+ elif source == ParameterSource.ENVIRONMENT and api_key_nonempty:
243
+ opts.api_key_from_flag = None
244
+ opts.api_key_from_env = api_key
245
+ else:
246
+ opts.api_key_from_flag = None
247
+ opts.api_key_from_env = None
248
+
249
+ # Keep opts.api_key populated for any code that still reads it directly.
250
+ if api_key:
251
+ opts.api_key = api_key
252
+ kwargs["opts"] = opts
253
+ return ctx.invoke(f, *args, **kwargs)
254
+
255
+ return wrapper
256
+
257
+
258
+ def _parse_suffixes(value):
259
+ """Split a CSV of trusted host suffixes into a normalised tuple."""
260
+ return tuple(
261
+ token.strip().lstrip(".") for token in (value or "").split(",") if token.strip()
262
+ )
263
+
264
+
265
+ def _guard_untrusted_endpoints(ctx, opts, host_suffixes, proxy_suffixes):
266
+ """Reject api_host/api_proxy redirections from directory-relative config.
267
+
268
+ Enforcement applies only when the effective value originated from an
269
+ untrusted (relative) config file and was not supplied via a CLI flag or
270
+ environment variable. Both must match their allow-listed host suffixes;
271
+ api_proxy has no public default suffixes, so it is rejected unless one is
272
+ supplied via the environment.
273
+ """
274
+ trusted_sources = (ParameterSource.COMMANDLINE, ParameterSource.ENVIRONMENT)
275
+ profile = ctx.meta.get("profile")
276
+
277
+ if ctx.get_parameter_source("api_host") not in trusted_sources:
278
+ relative_host = config.ConfigReader.read_relative_config_value(
279
+ "api_host", profile=profile
280
+ )
281
+ if relative_host and opts.api_host == relative_host:
282
+ validators.validate_untrusted_api_host(opts.api_host, host_suffixes)
283
+
284
+ if ctx.get_parameter_source("api_proxy") not in trusted_sources:
285
+ relative_proxy = config.ConfigReader.read_relative_config_value(
286
+ "api_proxy", profile=profile
287
+ )
288
+ if relative_proxy and opts.api_proxy == relative_proxy:
289
+ validators.validate_untrusted_api_proxy(opts.api_proxy, proxy_suffixes)
290
+
291
+
292
+ def initialise_session(f):
293
+ """Create a shared HTTP session with proxy/SSL/user-agent settings."""
294
+
295
+ @click.option(
296
+ "--api-host", envvar="CLOUDSMITH_API_HOST", help="The API host to connect to."
297
+ )
298
+ @click.option(
299
+ "--api-proxy",
300
+ envvar="CLOUDSMITH_API_PROXY",
301
+ help="The API proxy to connect through.",
302
+ )
303
+ @click.option(
304
+ "-S",
305
+ "--without-api-ssl-verify",
306
+ default=None,
307
+ is_flag=True,
308
+ envvar="CLOUDSMITH_WITHOUT_API_SSL_VERIFY",
309
+ help="Don't verify the SSL connection for the API. This is dangerous and "
310
+ "should only be used in an old/broken environment that doesn't support the "
311
+ "same secure ciphers as Cloudsmith.",
312
+ )
313
+ @click.option(
314
+ "--api-user-agent",
315
+ envvar="CLOUDSMITH_API_USER_AGENT",
316
+ help="The user agent to use for requests.",
317
+ )
318
+ @click.option(
319
+ "--api-headers",
320
+ envvar="CLOUDSMITH_API_HEADERS",
321
+ help="A CSV list of extra headers (key=value) to send to the API.",
322
+ )
323
+ @click.option(
324
+ "--allowed-api-host-suffixes",
325
+ envvar="CLOUDSMITH_ALLOWED_API_HOST_SUFFIXES",
326
+ hidden=True,
327
+ help="Comma-separated extra trusted api_host suffixes (internal use).",
328
+ )
329
+ @click.option(
330
+ "--allowed-api-proxy-suffixes",
331
+ envvar="CLOUDSMITH_ALLOWED_API_PROXY_SUFFIXES",
332
+ hidden=True,
333
+ help="Comma-separated trusted api_proxy host suffixes (internal use).",
334
+ )
335
+ @click.pass_context
336
+ @functools.wraps(f)
337
+ def wrapper(ctx, *args, **kwargs):
338
+ # pylint: disable=missing-docstring
339
+ opts = config.get_or_create_options(ctx)
340
+ host_suffixes = _parse_suffixes(kwargs.pop("allowed_api_host_suffixes"))
341
+ proxy_suffixes = _parse_suffixes(kwargs.pop("allowed_api_proxy_suffixes"))
342
+ opts.api_host = kwargs.pop("api_host")
343
+ opts.api_proxy = kwargs.pop("api_proxy")
344
+ opts.api_ssl_verify = _pop_boolean_flag(
345
+ kwargs, "without_api_ssl_verify", invert=True
346
+ )
347
+ opts.api_user_agent = kwargs.pop("api_user_agent")
348
+ opts.api_headers = kwargs.pop("api_headers")
349
+
350
+ _guard_untrusted_endpoints(ctx, opts, host_suffixes, proxy_suffixes)
351
+
352
+ opts.session = _create_session(
353
+ proxy=opts.api_proxy,
354
+ ssl_verify=opts.api_ssl_verify,
355
+ user_agent=opts.api_user_agent,
356
+ headers=opts.api_headers,
357
+ )
358
+
359
+ kwargs["opts"] = opts
360
+ return ctx.invoke(f, *args, **kwargs)
361
+
362
+ return wrapper
363
+
364
+
365
+ def _parse_detector_ids(value):
366
+ """Split a comma-separated detector id string into stripped, lowercased ids."""
367
+ return [
368
+ token.strip().lower() for token in (value or "").split(",") if token.strip()
369
+ ]
370
+
371
+
372
+ def _config_disabled_detectors(value):
373
+ """Detector ids disabled via the config-file ``oidc_disabled_detectors`` key."""
374
+ return frozenset(_parse_detector_ids(value))
375
+
376
+
377
+ def _warn_on_oidc_detector_controls(order, disabled):
378
+ """Warn when the OIDC detector order/disable controls look misconfigured.
379
+
380
+ Advisory only — detection itself is resolved in the core detectors module;
381
+ these warnings surface user-facing mistakes (a typo'd id, or controls that
382
+ leave nothing enabled) without aborting the credential fallback chain.
383
+ """
384
+ valid_ids = [detector_cls.id for detector_cls in registered_detectors()]
385
+ order_ids = _parse_detector_ids(order)
386
+
387
+ unknown_ids = [
388
+ identifier for identifier in order_ids if identifier not in valid_ids
389
+ ]
390
+ if unknown_ids:
391
+ click.secho(
392
+ "OIDC: ignoring unknown detector id(s) in --oidc-detector-order: "
393
+ f"{', '.join(unknown_ids)}. Valid ids: {', '.join(valid_ids)}.",
394
+ fg="yellow",
395
+ err=True,
396
+ )
397
+
398
+ candidate_ids = [i for i in order_ids if i in valid_ids] if order_ids else valid_ids
399
+ enabled_ids = [i for i in candidate_ids if i not in disabled]
400
+ if (order_ids or disabled) and not enabled_ids:
401
+ click.secho(
402
+ "OIDC: no detectors are enabled after applying the detector "
403
+ "order/disable controls; OIDC auto-discovery will be skipped.",
404
+ fg="yellow",
405
+ err=True,
406
+ )
407
+
408
+
409
+ def resolve_credentials(f):
410
+ """Resolve credentials via the provider chain. Depends on initialise_session."""
411
+
412
+ @click.option(
413
+ "--oidc-audience",
414
+ envvar="CLOUDSMITH_OIDC_AUDIENCE",
415
+ help="The OIDC audience for token requests.",
416
+ )
417
+ @click.option(
418
+ "--oidc-org",
419
+ envvar="CLOUDSMITH_ORG",
420
+ help="The Cloudsmith organisation slug for OIDC token exchange.",
421
+ )
422
+ @click.option(
423
+ "--oidc-service-slug",
424
+ envvar="CLOUDSMITH_SERVICE_SLUG",
425
+ help="The Cloudsmith service slug for OIDC token exchange.",
426
+ )
427
+ @click.option(
428
+ "--oidc-discovery-disabled",
429
+ default=None,
430
+ is_flag=True,
431
+ envvar="CLOUDSMITH_OIDC_DISCOVERY_DISABLED",
432
+ help="Disable OIDC auto-discovery.",
433
+ )
434
+ @click.option(
435
+ "--oidc-detector-order",
436
+ envvar="CLOUDSMITH_OIDC_DETECTOR_ORDER",
437
+ help="Comma-separated OIDC detector ids to control which detectors "
438
+ "are considered and the order they are tried in.",
439
+ )
440
+ @click.pass_context
441
+ @functools.wraps(f)
442
+ def wrapper(ctx, *args, **kwargs):
443
+ # pylint: disable=missing-docstring
444
+ opts = config.get_or_create_options(ctx)
445
+
446
+ oidc_audience = kwargs.pop("oidc_audience")
447
+ oidc_org = kwargs.pop("oidc_org")
448
+ oidc_service_slug = kwargs.pop("oidc_service_slug")
449
+ oidc_discovery_disabled = _pop_boolean_flag(kwargs, "oidc_discovery_disabled")
450
+ oidc_detector_order = kwargs.pop("oidc_detector_order")
451
+
452
+ if oidc_audience:
453
+ opts.oidc_audience = oidc_audience
454
+ if oidc_org:
455
+ opts.oidc_org = oidc_org
456
+ if oidc_service_slug:
457
+ opts.oidc_service_slug = oidc_service_slug
458
+ if oidc_discovery_disabled:
459
+ opts.oidc_discovery_disabled = oidc_discovery_disabled
460
+ if oidc_detector_order:
461
+ opts.oidc_detector_order = oidc_detector_order
462
+
463
+ oidc_disabled_detectors = disabled_detectors_from_env(
464
+ os.environ
465
+ ) | _config_disabled_detectors(opts.oidc_disabled_detectors)
466
+ _warn_on_oidc_detector_controls(
467
+ opts.oidc_detector_order, oidc_disabled_detectors
468
+ )
469
+
470
+ context = CredentialContext(
471
+ session=opts.session,
472
+ api_key_from_flag=opts.api_key_from_flag,
473
+ api_key_from_env=opts.api_key_from_env,
474
+ api_key_from_file=opts.api_key_from_file,
475
+ api_host=opts.api_host or "https://api.cloudsmith.io",
476
+ creds_file_path=ctx.meta.get("creds_file"),
477
+ profile=ctx.meta.get("profile"),
478
+ debug=opts.debug,
479
+ oidc_audience=opts.oidc_audience,
480
+ oidc_org=opts.oidc_org,
481
+ oidc_service_slug=opts.oidc_service_slug,
482
+ oidc_discovery_disabled=opts.oidc_discovery_disabled,
483
+ oidc_detector_order=opts.oidc_detector_order,
484
+ oidc_disabled_detectors=oidc_disabled_detectors,
485
+ )
486
+
487
+ chain = CredentialProviderChain()
488
+ credential = chain.resolve(context)
489
+
490
+ if context.keyring_refresh_failed:
491
+ click.secho(
492
+ "An error occurred when attempting to refresh your SSO access token. "
493
+ "To refresh this session, run 'cloudsmith auth'",
494
+ fg="yellow",
495
+ err=True,
496
+ )
497
+ if credential:
498
+ click.secho(
499
+ "Falling back to API key authentication.",
500
+ fg="yellow",
501
+ err=True,
502
+ )
503
+
504
+ opts.credential = credential
505
+
506
+ kwargs["opts"] = opts
507
+ return ctx.invoke(f, *args, **kwargs)
508
+
509
+ return initialise_session(wrapper)
510
+
511
+
512
+ def initialise_api(f):
513
+ """Initialise the Cloudsmith API for use. Depends on resolve_credentials."""
514
+
515
+ @click.option(
516
+ "-R",
517
+ "--without-rate-limit",
518
+ default=None,
519
+ is_flag=True,
520
+ help="Don't obey the suggested rate limit interval. The CLI will "
521
+ "otherwise automatically sleep between commands to ensure that you do "
522
+ "not hit the server-side rate limit.",
523
+ )
524
+ @click.option(
525
+ "--rate-limit-warning",
526
+ default=30,
527
+ help="When rate limiting, display information that it is happening "
528
+ "if wait interval is higher than this setting. By default no "
529
+ "information will be printed. Set to zero to always see it.",
530
+ )
531
+ @click.option(
532
+ "--error-retry-max",
533
+ default=6,
534
+ help="The maximum amount of times to retry on errors received from "
535
+ "the API, as determined by the --error-retry-codes parameter.",
536
+ )
537
+ @click.option(
538
+ "--error-retry-backoff",
539
+ default=1.0,
540
+ type=float,
541
+ help="The backoff factor determines how long to wait in seconds "
542
+ "between error retries. The backoff factor is multiplied by the "
543
+ "amount of retries so far. So if 1.0, then the wait is 1.0s then "
544
+ "2.0s, then 4.0s, and so forth.",
545
+ )
546
+ @click.option(
547
+ "--error-retry-codes",
548
+ default="429,500,502,503,504",
549
+ help="The status codes that when received from the API will cause "
550
+ "a retry (if --error-retry-max is > 0). By default this will be for "
551
+ "429, 500, 502, 503 and 504 error codes.",
552
+ )
553
+ @click.pass_context
554
+ @functools.wraps(f)
555
+ def wrapper(ctx, *args, **kwargs):
556
+ # pylint: disable=missing-docstring
557
+ opts = config.get_or_create_options(ctx)
558
+ opts.rate_limit = _pop_boolean_flag(kwargs, "without_rate_limit", invert=True)
559
+ opts.rate_limit_warning = kwargs.pop("rate_limit_warning")
560
+ opts.error_retry_max = kwargs.pop("error_retry_max")
561
+ opts.error_retry_backoff = kwargs.pop("error_retry_backoff")
562
+ opts.error_retry_codes = kwargs.pop("error_retry_codes")
563
+ opts.error_retry_cb = report_retry
564
+
565
+ def call_print_rate_limit_info_with_opts(rate_info):
566
+ utils.print_rate_limit_info(opts, rate_info)
567
+
568
+ opts.api_config = _initialise_api(
569
+ debug=opts.debug,
570
+ host=opts.api_host,
571
+ credential=opts.credential,
572
+ proxy=opts.api_proxy,
573
+ ssl_verify=opts.api_ssl_verify,
574
+ user_agent=opts.api_user_agent,
575
+ headers=opts.api_headers,
576
+ rate_limit=opts.rate_limit,
577
+ rate_limit_callback=call_print_rate_limit_info_with_opts,
578
+ error_retry_max=opts.error_retry_max,
579
+ error_retry_backoff=opts.error_retry_backoff,
580
+ error_retry_codes=opts.error_retry_codes,
581
+ error_retry_cb=opts.error_retry_cb,
582
+ )
583
+
584
+ kwargs["opts"] = opts
585
+ return ctx.invoke(f, *args, **kwargs)
586
+
587
+ return resolve_credentials(wrapper)
588
+
589
+
590
+ def initialise_mcp(f):
591
+ @click.option(
592
+ "-a",
593
+ "--all-tools",
594
+ default=False,
595
+ is_flag=True,
596
+ help="Show all tools",
597
+ )
598
+ @click.option(
599
+ "-D",
600
+ "--allow-destructive-tools",
601
+ default=False,
602
+ is_flag=True,
603
+ help="Allow destructive tools to be used",
604
+ )
605
+ @click.pass_context
606
+ @functools.wraps(f)
607
+ def wrapper(ctx, *args, **kwargs):
608
+ opts = kwargs.get("opts")
609
+
610
+ all_tools = kwargs.pop("all_tools")
611
+ allow_destructive_tools = kwargs.pop("allow_destructive_tools")
612
+
613
+ mcp_server = server.DynamicMCPServer(
614
+ api_config=opts.api_config,
615
+ debug_mode=opts.debug,
616
+ allow_destructive_tools=allow_destructive_tools,
617
+ allowed_tool_groups=opts.mcp_allowed_tool_groups,
618
+ allowed_tools=opts.mcp_allowed_tools,
619
+ force_all_tools=all_tools,
620
+ )
621
+ kwargs["mcp_server"] = mcp_server
622
+ return ctx.invoke(f, *args, **kwargs)
623
+
624
+ return wrapper