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.
- cloudsmith_cli/__init__.py +10 -0
- cloudsmith_cli/__main__.py +8 -0
- cloudsmith_cli/cli/__init__.py +1 -0
- cloudsmith_cli/cli/command.py +160 -0
- cloudsmith_cli/cli/commands/__init__.py +33 -0
- cloudsmith_cli/cli/commands/auth.py +173 -0
- cloudsmith_cli/cli/commands/check.py +129 -0
- cloudsmith_cli/cli/commands/copy.py +98 -0
- cloudsmith_cli/cli/commands/credential_helper/__init__.py +39 -0
- cloudsmith_cli/cli/commands/credential_helper/docker.py +66 -0
- cloudsmith_cli/cli/commands/credential_helper/manage.py +299 -0
- cloudsmith_cli/cli/commands/delete.py +67 -0
- cloudsmith_cli/cli/commands/dependencies.py +108 -0
- cloudsmith_cli/cli/commands/docs.py +16 -0
- cloudsmith_cli/cli/commands/download.py +620 -0
- cloudsmith_cli/cli/commands/entitlements.py +819 -0
- cloudsmith_cli/cli/commands/help_.py +12 -0
- cloudsmith_cli/cli/commands/list_.py +317 -0
- cloudsmith_cli/cli/commands/login.py +99 -0
- cloudsmith_cli/cli/commands/logout.py +151 -0
- cloudsmith_cli/cli/commands/main.py +73 -0
- cloudsmith_cli/cli/commands/mcp.py +523 -0
- cloudsmith_cli/cli/commands/metadata.py +503 -0
- cloudsmith_cli/cli/commands/metrics/__init__.py +2 -0
- cloudsmith_cli/cli/commands/metrics/command.py +20 -0
- cloudsmith_cli/cli/commands/metrics/entitlements.py +148 -0
- cloudsmith_cli/cli/commands/metrics/packages.py +134 -0
- cloudsmith_cli/cli/commands/move.py +111 -0
- cloudsmith_cli/cli/commands/policy/__init__.py +3 -0
- cloudsmith_cli/cli/commands/policy/command.py +20 -0
- cloudsmith_cli/cli/commands/policy/deny.py +248 -0
- cloudsmith_cli/cli/commands/policy/license.py +335 -0
- cloudsmith_cli/cli/commands/policy/vulnerability.py +322 -0
- cloudsmith_cli/cli/commands/push.py +1323 -0
- cloudsmith_cli/cli/commands/quarantine.py +148 -0
- cloudsmith_cli/cli/commands/quota/__init__.py +2 -0
- cloudsmith_cli/cli/commands/quota/command.py +20 -0
- cloudsmith_cli/cli/commands/quota/history.py +122 -0
- cloudsmith_cli/cli/commands/quota/quota.py +107 -0
- cloudsmith_cli/cli/commands/repos.py +330 -0
- cloudsmith_cli/cli/commands/resync.py +90 -0
- cloudsmith_cli/cli/commands/status.py +92 -0
- cloudsmith_cli/cli/commands/tags.py +375 -0
- cloudsmith_cli/cli/commands/tokens.py +318 -0
- cloudsmith_cli/cli/commands/upstream.py +479 -0
- cloudsmith_cli/cli/commands/vulnerabilities.py +141 -0
- cloudsmith_cli/cli/commands/whoami.py +188 -0
- cloudsmith_cli/cli/config.py +635 -0
- cloudsmith_cli/cli/decorators.py +624 -0
- cloudsmith_cli/cli/exceptions.py +215 -0
- cloudsmith_cli/cli/metadata_common.py +146 -0
- cloudsmith_cli/cli/saml.py +109 -0
- cloudsmith_cli/cli/table.py +59 -0
- cloudsmith_cli/cli/types.py +14 -0
- cloudsmith_cli/cli/utils.py +267 -0
- cloudsmith_cli/cli/validators.py +378 -0
- cloudsmith_cli/cli/webserver.py +263 -0
- cloudsmith_cli/core/__init__.py +1 -0
- cloudsmith_cli/core/api/__init__.py +1 -0
- cloudsmith_cli/core/api/distros.py +31 -0
- cloudsmith_cli/core/api/entitlements.py +130 -0
- cloudsmith_cli/core/api/exceptions.py +57 -0
- cloudsmith_cli/core/api/files.py +131 -0
- cloudsmith_cli/core/api/init.py +109 -0
- cloudsmith_cli/core/api/metadata.py +217 -0
- cloudsmith_cli/core/api/metrics.py +78 -0
- cloudsmith_cli/core/api/orgs.py +201 -0
- cloudsmith_cli/core/api/packages.py +309 -0
- cloudsmith_cli/core/api/quota.py +64 -0
- cloudsmith_cli/core/api/rates.py +28 -0
- cloudsmith_cli/core/api/repos.py +81 -0
- cloudsmith_cli/core/api/status.py +27 -0
- cloudsmith_cli/core/api/upstreams.py +72 -0
- cloudsmith_cli/core/api/user.py +109 -0
- cloudsmith_cli/core/api/version.py +15 -0
- cloudsmith_cli/core/api/vulnerabilities.py +230 -0
- cloudsmith_cli/core/cache_utils.py +160 -0
- cloudsmith_cli/core/config.py +140 -0
- cloudsmith_cli/core/credentials/__init__.py +0 -0
- cloudsmith_cli/core/credentials/chain.py +69 -0
- cloudsmith_cli/core/credentials/models.py +44 -0
- cloudsmith_cli/core/credentials/oidc/__init__.py +6 -0
- cloudsmith_cli/core/credentials/oidc/cache.py +220 -0
- cloudsmith_cli/core/credentials/oidc/detectors/__init__.py +122 -0
- cloudsmith_cli/core/credentials/oidc/detectors/aws.py +85 -0
- cloudsmith_cli/core/credentials/oidc/detectors/azure_devops.py +70 -0
- cloudsmith_cli/core/credentials/oidc/detectors/base.py +26 -0
- cloudsmith_cli/core/credentials/oidc/detectors/bitbucket_pipelines.py +35 -0
- cloudsmith_cli/core/credentials/oidc/detectors/circleci.py +40 -0
- cloudsmith_cli/core/credentials/oidc/detectors/generic.py +42 -0
- cloudsmith_cli/core/credentials/oidc/detectors/github_actions.py +64 -0
- cloudsmith_cli/core/credentials/oidc/detectors/gitlab_ci.py +49 -0
- cloudsmith_cli/core/credentials/oidc/exchange.py +87 -0
- cloudsmith_cli/core/credentials/provider.py +17 -0
- cloudsmith_cli/core/credentials/providers/__init__.py +15 -0
- cloudsmith_cli/core/credentials/providers/cli_flag.py +24 -0
- cloudsmith_cli/core/credentials/providers/credentials_file.py +24 -0
- cloudsmith_cli/core/credentials/providers/env_var.py +24 -0
- cloudsmith_cli/core/credentials/providers/keyring_provider.py +59 -0
- cloudsmith_cli/core/credentials/providers/oidc_provider.py +115 -0
- cloudsmith_cli/core/download.py +594 -0
- cloudsmith_cli/core/keyring.py +171 -0
- cloudsmith_cli/core/mcp/__init__.py +0 -0
- cloudsmith_cli/core/mcp/data.py +17 -0
- cloudsmith_cli/core/mcp/server.py +786 -0
- cloudsmith_cli/core/pagination.py +131 -0
- cloudsmith_cli/core/ratelimits.py +88 -0
- cloudsmith_cli/core/rest.py +255 -0
- cloudsmith_cli/core/utils.py +95 -0
- cloudsmith_cli/core/version.py +20 -0
- cloudsmith_cli/credential_helpers/__init__.py +7 -0
- cloudsmith_cli/credential_helpers/backends.py +41 -0
- cloudsmith_cli/credential_helpers/common.py +111 -0
- cloudsmith_cli/credential_helpers/custom_domains.py +281 -0
- cloudsmith_cli/credential_helpers/docker/__init__.py +4 -0
- cloudsmith_cli/credential_helpers/docker/installer.py +349 -0
- cloudsmith_cli/credential_helpers/docker/runtime.py +117 -0
- cloudsmith_cli/credential_helpers/launchers.py +175 -0
- cloudsmith_cli/data/VERSION +1 -0
- cloudsmith_cli/data/config.ini +23 -0
- cloudsmith_cli/data/credentials.ini +14 -0
- cloudsmith_cli/templates/__init__.py +3 -0
- cloudsmith_cli/templates/auth_error.html +45 -0
- cloudsmith_cli/templates/auth_success.html +37 -0
- cloudsmith_cli-1.20.0.dist-info/METADATA +610 -0
- cloudsmith_cli-1.20.0.dist-info/RECORD +130 -0
- cloudsmith_cli-1.20.0.dist-info/WHEEL +5 -0
- cloudsmith_cli-1.20.0.dist-info/entry_points.txt +2 -0
- cloudsmith_cli-1.20.0.dist-info/licenses/LICENSE +201 -0
- cloudsmith_cli-1.20.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Copyright 2026 Cloudsmith Ltd
|
|
2
|
+
"""GitHub Actions OIDC detector.
|
|
3
|
+
|
|
4
|
+
Fetches an OIDC token via the Actions runtime HTTP endpoint, using the
|
|
5
|
+
``ACTIONS_ID_TOKEN_REQUEST_URL`` and ``ACTIONS_ID_TOKEN_REQUEST_TOKEN``
|
|
6
|
+
variables exposed when a workflow requests ``id-token: write`` permission.
|
|
7
|
+
|
|
8
|
+
References:
|
|
9
|
+
https://docs.github.com/en/actions/reference/security/oidc
|
|
10
|
+
https://docs.cloudsmith.com/authentication/setup-cloudsmith-to-authenticate-with-oidc-in-github-actions
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
from urllib.parse import quote
|
|
17
|
+
|
|
18
|
+
from ....rest import create_requests_session as create_session
|
|
19
|
+
from .base import EnvironmentDetector
|
|
20
|
+
|
|
21
|
+
DEFAULT_AUDIENCE = "cloudsmith"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GitHubActionsDetector(EnvironmentDetector):
|
|
25
|
+
"""Detects GitHub Actions and fetches an OIDC token via HTTP request."""
|
|
26
|
+
|
|
27
|
+
name = "GitHub Actions"
|
|
28
|
+
id = "github"
|
|
29
|
+
|
|
30
|
+
def detect(self) -> bool:
|
|
31
|
+
return (
|
|
32
|
+
os.environ.get("GITHUB_ACTIONS") == "true"
|
|
33
|
+
and bool(os.environ.get("ACTIONS_ID_TOKEN_REQUEST_URL"))
|
|
34
|
+
and bool(os.environ.get("ACTIONS_ID_TOKEN_REQUEST_TOKEN"))
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def get_token(self) -> str:
|
|
38
|
+
request_url = os.environ["ACTIONS_ID_TOKEN_REQUEST_URL"]
|
|
39
|
+
request_token = os.environ["ACTIONS_ID_TOKEN_REQUEST_TOKEN"]
|
|
40
|
+
|
|
41
|
+
audience = self.context.oidc_audience or DEFAULT_AUDIENCE
|
|
42
|
+
separator = "&" if "?" in request_url else "?"
|
|
43
|
+
url = f"{request_url}{separator}audience={quote(audience, safe='')}"
|
|
44
|
+
|
|
45
|
+
session = self.context.session or create_session()
|
|
46
|
+
try:
|
|
47
|
+
response = session.get(
|
|
48
|
+
url,
|
|
49
|
+
headers={
|
|
50
|
+
"Authorization": f"Bearer {request_token}",
|
|
51
|
+
"Accept": "application/json; api-version=2.0",
|
|
52
|
+
},
|
|
53
|
+
timeout=30,
|
|
54
|
+
)
|
|
55
|
+
response.raise_for_status()
|
|
56
|
+
|
|
57
|
+
data = response.json()
|
|
58
|
+
token = data.get("value")
|
|
59
|
+
if not token:
|
|
60
|
+
raise ValueError("GitHub Actions OIDC response did not contain a token")
|
|
61
|
+
return token
|
|
62
|
+
finally:
|
|
63
|
+
if not self.context.session:
|
|
64
|
+
session.close()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Copyright 2026 Cloudsmith Ltd
|
|
2
|
+
"""GitLab CI OIDC detector.
|
|
3
|
+
|
|
4
|
+
Reads an OIDC token from environment variables populated by GitLab's
|
|
5
|
+
``id_tokens`` configuration in ``.gitlab-ci.yml``.
|
|
6
|
+
|
|
7
|
+
References:
|
|
8
|
+
https://docs.gitlab.com/ci/cloud_services/
|
|
9
|
+
https://docs.cloudsmith.com/integrations/integrating-with-gitlab-cicd
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
|
|
16
|
+
from .base import EnvironmentDetector
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GitLabCIDetector(EnvironmentDetector):
|
|
20
|
+
"""Detects GitLab CI and reads an OIDC token from an environment variable.
|
|
21
|
+
|
|
22
|
+
GitLab requires users to configure ``id_tokens`` in ``.gitlab-ci.yml``,
|
|
23
|
+
minting a token with ``aud`` set to the Cloudsmith OIDC endpoint and
|
|
24
|
+
exposing it as ``CLOUDSMITH_OIDC_TOKEN``. The legacy ``CI_JOB_JWT``/
|
|
25
|
+
``CI_JOB_JWT_V2`` variables are deliberately not consulted: they were
|
|
26
|
+
removed in GitLab 17.0, carry the GitLab instance URL as their audience
|
|
27
|
+
(not the Cloudsmith audience the token exchange validates), and were
|
|
28
|
+
auto-injected into every job on older instances.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
name = "GitLab CI"
|
|
32
|
+
id = "gitlab"
|
|
33
|
+
|
|
34
|
+
TOKEN_ENV_VAR = "CLOUDSMITH_OIDC_TOKEN"
|
|
35
|
+
|
|
36
|
+
def detect(self) -> bool:
|
|
37
|
+
if os.environ.get("GITLAB_CI") != "true":
|
|
38
|
+
return False
|
|
39
|
+
return bool(os.environ.get(self.TOKEN_ENV_VAR))
|
|
40
|
+
|
|
41
|
+
def get_token(self) -> str:
|
|
42
|
+
token = os.environ.get(self.TOKEN_ENV_VAR)
|
|
43
|
+
if token:
|
|
44
|
+
return token
|
|
45
|
+
raise ValueError(
|
|
46
|
+
"GitLab CI detected but no OIDC token found. "
|
|
47
|
+
"Configure id_tokens in .gitlab-ci.yml and expose it as "
|
|
48
|
+
+ self.TOKEN_ENV_VAR
|
|
49
|
+
)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Cloudsmith OIDC token exchange.
|
|
2
|
+
|
|
3
|
+
Exchanges a vendor OIDC JWT for a short-lived Cloudsmith API token
|
|
4
|
+
via the POST /openid/{org}/ endpoint.
|
|
5
|
+
|
|
6
|
+
References:
|
|
7
|
+
https://help.cloudsmith.io/docs/openid-connect
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
import requests
|
|
16
|
+
|
|
17
|
+
from ...rest import create_requests_session as create_session
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from ... import CredentialContext
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def exchange_oidc_token(
|
|
26
|
+
context: CredentialContext,
|
|
27
|
+
org: str,
|
|
28
|
+
service_slug: str,
|
|
29
|
+
oidc_token: str,
|
|
30
|
+
) -> str:
|
|
31
|
+
"""Exchange a vendor OIDC JWT for a Cloudsmith API token.
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
OidcExchangeError: If the exchange fails.
|
|
35
|
+
"""
|
|
36
|
+
host = context.api_host.rstrip("/")
|
|
37
|
+
if not host.startswith("http"):
|
|
38
|
+
host = f"https://{host}"
|
|
39
|
+
|
|
40
|
+
url = f"{host}/openid/{org}/"
|
|
41
|
+
payload = {
|
|
42
|
+
"oidc_token": oidc_token,
|
|
43
|
+
"service_slug": service_slug,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
session = context.session or create_session()
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
try:
|
|
50
|
+
response = session.post(
|
|
51
|
+
url,
|
|
52
|
+
json=payload,
|
|
53
|
+
timeout=30,
|
|
54
|
+
)
|
|
55
|
+
except requests.exceptions.RequestException as exc:
|
|
56
|
+
raise OidcExchangeError(
|
|
57
|
+
f"OIDC token exchange request failed: {exc}"
|
|
58
|
+
) from exc
|
|
59
|
+
|
|
60
|
+
if response.status_code in (200, 201):
|
|
61
|
+
data = response.json()
|
|
62
|
+
token = data.get("token")
|
|
63
|
+
if not token or not isinstance(token, str) or not token.strip():
|
|
64
|
+
raise OidcExchangeError(
|
|
65
|
+
"Cloudsmith OIDC exchange returned an empty or invalid token"
|
|
66
|
+
)
|
|
67
|
+
return token
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
error_json = response.json()
|
|
71
|
+
error_detail = error_json.get(
|
|
72
|
+
"detail", error_json.get("error", str(error_json))
|
|
73
|
+
)
|
|
74
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
75
|
+
error_detail = response.text[:200]
|
|
76
|
+
|
|
77
|
+
raise OidcExchangeError(
|
|
78
|
+
f"OIDC token exchange failed with {response.status_code}: "
|
|
79
|
+
f"{error_detail}"
|
|
80
|
+
)
|
|
81
|
+
finally:
|
|
82
|
+
if not context.session:
|
|
83
|
+
session.close()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class OidcExchangeError(Exception):
|
|
87
|
+
"""Raised when the OIDC token exchange with Cloudsmith fails."""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Base credential provider interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
|
|
7
|
+
from .models import CredentialContext, CredentialResult
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CredentialProvider(ABC):
|
|
11
|
+
"""Base class for credential providers."""
|
|
12
|
+
|
|
13
|
+
name: str = "base"
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def resolve(self, context: CredentialContext) -> CredentialResult | None:
|
|
17
|
+
"""Attempt to resolve credentials. Return CredentialResult or None."""
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Credential providers for the Cloudsmith CLI."""
|
|
2
|
+
|
|
3
|
+
from .cli_flag import CLIFlagProvider
|
|
4
|
+
from .credentials_file import CredentialsFileProvider
|
|
5
|
+
from .env_var import EnvVarProvider
|
|
6
|
+
from .keyring_provider import KeyringProvider
|
|
7
|
+
from .oidc_provider import OidcProvider
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"CLIFlagProvider",
|
|
11
|
+
"CredentialsFileProvider",
|
|
12
|
+
"EnvVarProvider",
|
|
13
|
+
"KeyringProvider",
|
|
14
|
+
"OidcProvider",
|
|
15
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""CLI flag credential provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..models import CredentialContext, CredentialResult
|
|
6
|
+
from ..provider import CredentialProvider
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CLIFlagProvider(CredentialProvider):
|
|
10
|
+
"""Resolves credentials from the --api-key CLI flag."""
|
|
11
|
+
|
|
12
|
+
name = "cli_flag"
|
|
13
|
+
|
|
14
|
+
def resolve(self, context: CredentialContext) -> CredentialResult | None:
|
|
15
|
+
api_key = context.api_key_from_flag
|
|
16
|
+
if api_key and api_key.strip():
|
|
17
|
+
api_key = api_key.strip()
|
|
18
|
+
suffix = api_key[-4:]
|
|
19
|
+
return CredentialResult(
|
|
20
|
+
api_key=api_key,
|
|
21
|
+
source_name="cli_flag",
|
|
22
|
+
source_detail=f"--api-key flag (ends with ...{suffix})",
|
|
23
|
+
)
|
|
24
|
+
return None
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Credentials file provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..models import CredentialContext, CredentialResult
|
|
6
|
+
from ..provider import CredentialProvider
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CredentialsFileProvider(CredentialProvider):
|
|
10
|
+
"""Resolves credentials from the api_key stored in credentials.ini."""
|
|
11
|
+
|
|
12
|
+
name = "credentials_file"
|
|
13
|
+
|
|
14
|
+
def resolve(self, context: CredentialContext) -> CredentialResult | None:
|
|
15
|
+
api_key = context.api_key_from_file
|
|
16
|
+
if api_key and api_key.strip():
|
|
17
|
+
api_key = api_key.strip()
|
|
18
|
+
suffix = api_key[-4:]
|
|
19
|
+
return CredentialResult(
|
|
20
|
+
api_key=api_key,
|
|
21
|
+
source_name="credentials_file",
|
|
22
|
+
source_detail=f"credentials.ini (ends with ...{suffix})",
|
|
23
|
+
)
|
|
24
|
+
return None
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Environment variable credential provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..models import CredentialContext, CredentialResult
|
|
6
|
+
from ..provider import CredentialProvider
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EnvVarProvider(CredentialProvider):
|
|
10
|
+
"""Resolves credentials from the CLOUDSMITH_API_KEY environment variable."""
|
|
11
|
+
|
|
12
|
+
name = "env_var"
|
|
13
|
+
|
|
14
|
+
def resolve(self, context: CredentialContext) -> CredentialResult | None:
|
|
15
|
+
api_key = context.api_key_from_env
|
|
16
|
+
if api_key and api_key.strip():
|
|
17
|
+
api_key = api_key.strip()
|
|
18
|
+
suffix = api_key[-4:]
|
|
19
|
+
return CredentialResult(
|
|
20
|
+
api_key=api_key,
|
|
21
|
+
source_name="env_var",
|
|
22
|
+
source_detail=f"CLOUDSMITH_API_KEY env var (ends with ...{suffix})",
|
|
23
|
+
)
|
|
24
|
+
return None
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Keyring credential provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from ....cli.saml import refresh_access_token
|
|
8
|
+
from ....core import keyring
|
|
9
|
+
from ..models import CredentialContext, CredentialResult
|
|
10
|
+
from ..provider import CredentialProvider
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class KeyringProvider(CredentialProvider):
|
|
16
|
+
"""Resolves credentials from SAML tokens stored in the system keyring."""
|
|
17
|
+
|
|
18
|
+
name = "keyring"
|
|
19
|
+
|
|
20
|
+
def resolve(self, context: CredentialContext) -> CredentialResult | None:
|
|
21
|
+
if not keyring.should_use_keyring():
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
api_host = context.api_host
|
|
25
|
+
access_token = keyring.get_access_token(api_host)
|
|
26
|
+
|
|
27
|
+
if not access_token:
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
if keyring.should_refresh_access_token(api_host):
|
|
32
|
+
if not context.session:
|
|
33
|
+
logger.debug(
|
|
34
|
+
"Session unavailable; skipping token refresh, using existing token"
|
|
35
|
+
)
|
|
36
|
+
else:
|
|
37
|
+
refresh_token = keyring.get_refresh_token(api_host)
|
|
38
|
+
new_access_token, new_refresh_token = refresh_access_token(
|
|
39
|
+
api_host,
|
|
40
|
+
access_token,
|
|
41
|
+
refresh_token,
|
|
42
|
+
session=context.session,
|
|
43
|
+
)
|
|
44
|
+
keyring.store_sso_tokens(
|
|
45
|
+
api_host, new_access_token, new_refresh_token
|
|
46
|
+
)
|
|
47
|
+
access_token = new_access_token
|
|
48
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
49
|
+
keyring.update_refresh_attempted_at(api_host)
|
|
50
|
+
context.keyring_refresh_failed = True
|
|
51
|
+
logger.debug("Failed to refresh SAML token", exc_info=True)
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
return CredentialResult(
|
|
55
|
+
api_key=access_token,
|
|
56
|
+
source_name="keyring",
|
|
57
|
+
source_detail="SAML token from system keyring",
|
|
58
|
+
auth_type="bearer",
|
|
59
|
+
)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""OIDC credential provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from ..models import CredentialContext, CredentialResult
|
|
8
|
+
from ..provider import CredentialProvider
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OidcProvider(CredentialProvider):
|
|
14
|
+
"""Resolves credentials via OIDC auto-discovery.
|
|
15
|
+
|
|
16
|
+
Requires CLOUDSMITH_ORG and CLOUDSMITH_SERVICE_SLUG to be set (via env
|
|
17
|
+
vars or click options). Auto-detects the environment, fetches the vendor
|
|
18
|
+
OIDC JWT, and exchanges it for a short-lived Cloudsmith API token.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
name = "oidc"
|
|
22
|
+
|
|
23
|
+
def resolve( # pylint: disable=too-many-return-statements
|
|
24
|
+
self, context: CredentialContext
|
|
25
|
+
) -> CredentialResult | None:
|
|
26
|
+
if context.oidc_discovery_disabled:
|
|
27
|
+
if context.debug:
|
|
28
|
+
logger.debug(
|
|
29
|
+
"OidcProvider: OIDC auto-discovery disabled via "
|
|
30
|
+
"CLOUDSMITH_OIDC_DISCOVERY_DISABLED"
|
|
31
|
+
)
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
org = context.oidc_org
|
|
35
|
+
service_slug = context.oidc_service_slug
|
|
36
|
+
|
|
37
|
+
if not org or not service_slug:
|
|
38
|
+
if context.debug:
|
|
39
|
+
logger.debug(
|
|
40
|
+
"OidcProvider: CLOUDSMITH_ORG and/or CLOUDSMITH_SERVICE_SLUG "
|
|
41
|
+
"not set, skipping OIDC auto-discovery"
|
|
42
|
+
)
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
from ..oidc.cache import get_cached_token, store_cached_token
|
|
46
|
+
|
|
47
|
+
# Check cache BEFORE environment detection — detection can be expensive
|
|
48
|
+
# (e.g. boto3 credential resolution, IMDS calls) and is unnecessary when
|
|
49
|
+
# we already hold a valid exchanged token.
|
|
50
|
+
cached = get_cached_token(context.api_host, org, service_slug)
|
|
51
|
+
if cached:
|
|
52
|
+
logger.debug("OidcProvider: Using cached OIDC token")
|
|
53
|
+
return CredentialResult(
|
|
54
|
+
api_key=cached,
|
|
55
|
+
source_name="oidc",
|
|
56
|
+
source_detail=f"OIDC [cached] (org: {org}, service: {service_slug})",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
from ..oidc.detectors import detect_environment
|
|
60
|
+
from ..oidc.exchange import OidcExchangeError, exchange_oidc_token
|
|
61
|
+
|
|
62
|
+
detector = detect_environment(context=context)
|
|
63
|
+
if detector is None:
|
|
64
|
+
if context.debug:
|
|
65
|
+
logger.debug(
|
|
66
|
+
"OidcProvider: No supported OIDC environment detected, skipping"
|
|
67
|
+
)
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
vendor_token = detector.get_token()
|
|
72
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
73
|
+
logger.warning(
|
|
74
|
+
"OIDC: Failed to retrieve identity token from %s. "
|
|
75
|
+
"Use --debug for details.",
|
|
76
|
+
detector.name,
|
|
77
|
+
)
|
|
78
|
+
logger.debug(
|
|
79
|
+
"OidcProvider: %s token retrieval error",
|
|
80
|
+
detector.name,
|
|
81
|
+
exc_info=True,
|
|
82
|
+
)
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
if not vendor_token:
|
|
86
|
+
logger.warning("OIDC: %s detector returned an empty token.", detector.name)
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
cloudsmith_token = exchange_oidc_token(
|
|
91
|
+
context=context,
|
|
92
|
+
org=org,
|
|
93
|
+
service_slug=service_slug,
|
|
94
|
+
oidc_token=vendor_token,
|
|
95
|
+
)
|
|
96
|
+
except OidcExchangeError as exc:
|
|
97
|
+
logger.warning("OIDC: Token exchange failed: %s", exc)
|
|
98
|
+
return None
|
|
99
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
100
|
+
logger.warning(
|
|
101
|
+
"OIDC: Token exchange failed unexpectedly. Use --debug for details."
|
|
102
|
+
)
|
|
103
|
+
logger.debug("OidcProvider: OIDC token exchange error", exc_info=True)
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
if not cloudsmith_token:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
store_cached_token(context.api_host, org, service_slug, cloudsmith_token)
|
|
110
|
+
|
|
111
|
+
return CredentialResult(
|
|
112
|
+
api_key=cloudsmith_token,
|
|
113
|
+
source_name="oidc",
|
|
114
|
+
source_detail=f"OIDC via {detector.name} (org: {org}, service: {service_slug})",
|
|
115
|
+
)
|