Skip to content

Commit cad5921

Browse files
feat: Add credential helpers for package manager authentication
Add credential helpers that automatically authenticate package managers with Cloudsmith registries using the credential provider chain (Environment Variable → Config File → Keyring → OIDC). Supported formats: Docker docker-credential-cloudsmith Terraform terraform-credentials-cloudsmith Cargo cargo-credential-cloudsmith pnpm cloudsmith-token-helper NuGet CredentialProvider.Cloudsmith pip/twine keyring backend (auto-discovered) Conda conda plugin (auto-discovered) ## Docker Configure `~/.docker/config.json`: { "credHelpers": { "docker.cloudsmith.io": "cloudsmith" } } Then: docker pull docker.cloudsmith.io/myorg/myrepo/myimage:latest ## Terraform Install the helper and configure `~/.terraformrc`: mkdir -p ~/.terraform.d/plugins ln -sf "$(which terraform-credentials-cloudsmith)" ~/.terraform.d/plugins/ credentials_helper "cloudsmith" { args = [] } Requires CLOUDSMITH_ORG and CLOUDSMITH_REPO environment variables. Token format is org/repo/token per Cloudsmith's Terraform registry API. ## Cargo Configure `~/.cargo/config.toml`: [registries.cloudsmith] index = "sparse+https://cargo.cloudsmith.io/myorg/myrepo/" credential-provider = ["cargo-credential-cloudsmith"] Then: cargo add my-crate --registry cloudsmith ## pnpm Configure `~/.npmrc` (requires absolute path to helper): registry=https://npm.cloudsmith.io/myorg/myrepo/ //npm.cloudsmith.io/myorg/myrepo/:tokenHelper=/usr/local/bin/cloudsmith-token-helper Returns "Bearer <token>" as pnpm does not auto-add the prefix. ## NuGet Set NUGET_CREDENTIALPROVIDERS_PATH to the directory containing CredentialProvider.Cloudsmith and add a package source: <packageSources> <add key="cloudsmith" value="https://nuget.cloudsmith.io/myorg/myrepo/v3/index.json" /> </packageSources> Then: dotnet restore ## pip / twine Auto-discovered via the keyring.backends entry point. No configuration needed beyond installing cloudsmith-cli: pip install --index-url=https://dl.cloudsmith.io/basic/myorg/myrepo/python/simple/ mypkg ## Conda Auto-discovered via the conda plugin entry point. Install cloudsmith-cli into conda's base Python environment. Configure `~/.condarc`: channel_settings: - channel: https://conda.cloudsmith.io/myorg/myrepo/ auth: cloudsmith channels: - https://conda.cloudsmith.io/myorg/myrepo/ - defaults Then: conda install my-package ## Architecture - cloudsmith_cli/credential_helpers/common.py: shared resolve_credentials(), extract_hostname(), is_cloudsmith_domain() used by all helpers - CredentialProviderChain defaults to the standard 4-provider chain - Networking config (proxy, TLS, headers) read from CLOUDSMITH_API_PROXY, CLOUDSMITH_WITHOUT_API_SSL_VERIFY, CLOUDSMITH_API_USER_AGENT, CLOUDSMITH_API_HEADERS env vars and config.ini - Custom domain discovery via GET /orgs/{org}/custom-domains/ with 1-hour filesystem cache in ~/.cloudsmith/cache/custom_domains/ Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent df70f37 commit cad5921

26 files changed

Lines changed: 1648 additions & 5 deletions

File tree

cloudsmith_cli/cli/commands/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
auth,
55
check,
66
copy,
7+
credential_helper,
78
delete,
89
dependencies,
910
docs,
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""
2+
Credential helper commands for Cloudsmith.
3+
4+
This module provides credential helper commands for various package managers
5+
(Docker, pip, npm, etc.) that follow their respective credential helper protocols.
6+
"""
7+
8+
import click
9+
10+
from ..main import main
11+
from .cargo import cargo as cargo_cmd
12+
from .conda import conda as conda_cmd
13+
from .docker import docker as docker_cmd
14+
from .npm import npm as npm_cmd
15+
from .nuget import nuget as nuget_cmd
16+
from .terraform import terraform as terraform_cmd
17+
18+
19+
@click.group()
20+
def credential_helper():
21+
"""
22+
Credential helpers for package managers.
23+
24+
These commands provide credentials for package managers like Docker, pip,
25+
npm, Terraform, Cargo, Conda, and NuGet. They are typically called by
26+
wrapper binaries (e.g., docker-credential-cloudsmith) or used directly
27+
for debugging.
28+
29+
Examples:
30+
# Test Docker credential helper
31+
$ echo "docker.cloudsmith.io" | cloudsmith credential-helper docker
32+
33+
# Test Terraform credential helper
34+
$ cloudsmith credential-helper terraform terraform.cloudsmith.io
35+
36+
# Test npm/pnpm token helper
37+
$ cloudsmith credential-helper npm
38+
"""
39+
40+
41+
# Register subcommands
42+
credential_helper.add_command(cargo_cmd, name="cargo")
43+
credential_helper.add_command(conda_cmd, name="conda")
44+
credential_helper.add_command(docker_cmd, name="docker")
45+
credential_helper.add_command(npm_cmd, name="npm")
46+
credential_helper.add_command(nuget_cmd, name="nuget")
47+
credential_helper.add_command(terraform_cmd, name="terraform")
48+
49+
# Register with main CLI
50+
main.add_command(credential_helper, name="credential-helper")
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""
2+
Cargo credential helper command.
3+
4+
Implements credential retrieval for Cargo registries hosted on Cloudsmith.
5+
"""
6+
7+
import json
8+
import sys
9+
10+
import click
11+
12+
from ....credential_helpers.cargo import get_credentials
13+
14+
15+
@click.command()
16+
@click.argument("index_url", required=False, default=None)
17+
def cargo(index_url):
18+
"""
19+
Cargo credential helper for Cloudsmith registries.
20+
21+
Returns credentials as a Bearer token for Cargo sparse registries.
22+
23+
If INDEX_URL is provided as an argument, uses it directly.
24+
Otherwise reads from stdin.
25+
26+
Examples:
27+
# Direct usage
28+
$ cloudsmith credential-helper cargo sparse+https://cargo.cloudsmith.io/org/repo/
29+
Bearer eyJ0eXAiOiJKV1Qi...
30+
31+
# Via wrapper (called by Cargo)
32+
$ cargo-credential-cloudsmith --cargo-plugin
33+
34+
Environment variables:
35+
CLOUDSMITH_API_KEY: API key for authentication (optional)
36+
CLOUDSMITH_ORG: Organization slug (required for OIDC)
37+
CLOUDSMITH_SERVICE_SLUG: Service account slug (required for OIDC)
38+
"""
39+
try:
40+
if not index_url:
41+
index_url = sys.stdin.read().strip()
42+
43+
if not index_url:
44+
click.echo("Error: No index URL provided", err=True)
45+
sys.exit(1)
46+
47+
token = get_credentials(index_url, debug=False)
48+
49+
if not token:
50+
click.echo(
51+
"Error: Unable to retrieve credentials. "
52+
"Set CLOUDSMITH_API_KEY or configure OIDC.",
53+
err=True,
54+
)
55+
sys.exit(1)
56+
57+
click.echo(json.dumps({"token": f"Bearer {token}"}))
58+
59+
except Exception as e: # pylint: disable=broad-exception-caught
60+
click.echo(f"Error: {e}", err=True)
61+
sys.exit(1)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""
2+
Conda credential helper command.
3+
4+
Provides credential retrieval for Conda channels hosted on Cloudsmith.
5+
"""
6+
7+
import json
8+
import sys
9+
10+
import click
11+
12+
from ....credential_helpers.conda import get_credentials
13+
14+
15+
@click.command()
16+
@click.argument("channel_url", required=False, default=None)
17+
def conda(channel_url):
18+
"""
19+
Conda credential helper for Cloudsmith channels.
20+
21+
Returns credentials as JSON for Cloudsmith Conda channels.
22+
23+
If CHANNEL_URL is provided as an argument, uses it directly.
24+
Otherwise reads from stdin.
25+
26+
Examples:
27+
# Direct usage
28+
$ cloudsmith credential-helper conda https://conda.cloudsmith.io/org/repo/
29+
{"username":"token","password":"eyJ0eXAiOiJKV1Qi..."}
30+
31+
The conda plugin (cloudsmith_cli.credential_helpers.conda.plugin) provides
32+
automatic authentication when installed as a conda plugin.
33+
34+
Environment variables:
35+
CLOUDSMITH_API_KEY: API key for authentication (optional)
36+
CLOUDSMITH_ORG: Organization slug (required for OIDC)
37+
CLOUDSMITH_SERVICE_SLUG: Service account slug (required for OIDC)
38+
"""
39+
try:
40+
if not channel_url:
41+
channel_url = sys.stdin.read().strip()
42+
43+
if not channel_url:
44+
click.echo("Error: No channel URL provided", err=True)
45+
sys.exit(1)
46+
47+
creds = get_credentials(channel_url, debug=False)
48+
49+
if not creds:
50+
click.echo(
51+
"Error: Unable to retrieve credentials. "
52+
"Set CLOUDSMITH_API_KEY or configure OIDC.",
53+
err=True,
54+
)
55+
sys.exit(1)
56+
57+
username, password = creds
58+
click.echo(json.dumps({"username": username, "password": password}))
59+
60+
except Exception as e: # pylint: disable=broad-exception-caught
61+
click.echo(f"Error: {e}", err=True)
62+
sys.exit(1)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""
2+
Docker credential helper command.
3+
4+
Implements the Docker credential helper protocol for Cloudsmith registries.
5+
"""
6+
7+
import json
8+
import sys
9+
10+
import click
11+
12+
from ....credential_helpers.docker import get_credentials
13+
14+
15+
@click.command()
16+
def docker():
17+
"""
18+
Docker credential helper for Cloudsmith registries.
19+
20+
Reads a Docker registry server URL from stdin and returns credentials in JSON format.
21+
This command implements the 'get' operation of the Docker credential helper protocol.
22+
23+
Only provides credentials for Cloudsmith Docker registries (docker.cloudsmith.io).
24+
25+
Input (stdin):
26+
Server URL as plain text (e.g., "docker.cloudsmith.io")
27+
28+
Output (stdout):
29+
JSON: {"Username": "token", "Secret": "<cloudsmith-token>"}
30+
31+
Exit codes:
32+
0: Success
33+
1: Error (no credentials available, not a Cloudsmith registry, etc.)
34+
35+
Examples:
36+
# Manual testing
37+
$ echo "docker.cloudsmith.io" | cloudsmith credential-helper docker
38+
{"Username":"token","Secret":"eyJ0eXAiOiJKV1Qi..."}
39+
40+
# Called by Docker via wrapper
41+
$ echo "docker.cloudsmith.io" | docker-credential-cloudsmith get
42+
{"Username":"token","Secret":"eyJ0eXAiOiJKV1Qi..."}
43+
44+
Environment variables:
45+
CLOUDSMITH_API_KEY: API key for authentication (optional)
46+
CLOUDSMITH_ORG: Organization slug (required for OIDC)
47+
CLOUDSMITH_SERVICE_SLUG: Service account slug (required for OIDC)
48+
"""
49+
try:
50+
# Read server URL from stdin
51+
server_url = sys.stdin.read().strip()
52+
53+
if not server_url:
54+
click.echo("Error: No server URL provided on stdin", err=True)
55+
sys.exit(1)
56+
57+
# Get credentials using the credential provider chain
58+
credentials = get_credentials(server_url, debug=False)
59+
60+
if not credentials:
61+
click.echo(
62+
"Error: Unable to retrieve credentials. "
63+
"Make sure you have either CLOUDSMITH_API_KEY set, "
64+
"or CLOUDSMITH_ORG + CLOUDSMITH_SERVICE_SLUG for OIDC authentication.",
65+
err=True,
66+
)
67+
sys.exit(1)
68+
69+
# Output credentials in Docker credential helper JSON format
70+
click.echo(json.dumps(credentials))
71+
72+
except Exception as e: # pylint: disable=broad-exception-caught
73+
# Broad exception catch to ensure we never crash without a message
74+
click.echo(f"Error: {e}", err=True)
75+
sys.exit(1)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""
2+
npm/pnpm token helper command.
3+
4+
Prints a raw Cloudsmith API token for use with pnpm's tokenHelper.
5+
"""
6+
7+
import sys
8+
9+
import click
10+
11+
from ....credential_helpers.npm import get_token
12+
13+
14+
@click.command()
15+
def npm():
16+
"""
17+
npm/pnpm token helper for Cloudsmith registries.
18+
19+
Prints a raw API token to stdout for use with pnpm's tokenHelper configuration.
20+
21+
Examples:
22+
# Direct usage
23+
$ cloudsmith credential-helper npm
24+
eyJ0eXAiOiJKV1Qi...
25+
26+
# Via wrapper (called by pnpm)
27+
$ cloudsmith-token-helper
28+
eyJ0eXAiOiJKV1Qi...
29+
30+
Configuration in ~/.npmrc:
31+
//npm.cloudsmith.io/:tokenHelper=/absolute/path/to/cloudsmith-token-helper
32+
33+
Find the path with: which cloudsmith-token-helper
34+
35+
Environment variables:
36+
CLOUDSMITH_API_KEY: API key for authentication (optional)
37+
CLOUDSMITH_ORG: Organization slug (required for OIDC)
38+
CLOUDSMITH_SERVICE_SLUG: Service account slug (required for OIDC)
39+
"""
40+
try:
41+
token = get_token(debug=False)
42+
43+
if not token:
44+
click.echo(
45+
"Error: Unable to retrieve credentials. "
46+
"Set CLOUDSMITH_API_KEY or configure OIDC.",
47+
err=True,
48+
)
49+
sys.exit(1)
50+
51+
# Raw token output — no JSON, no trailing newline issues
52+
sys.stdout.write(token)
53+
sys.stdout.flush()
54+
55+
except Exception as e: # pylint: disable=broad-exception-caught
56+
click.echo(f"Error: {e}", err=True)
57+
sys.exit(1)
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""
2+
NuGet credential helper command.
3+
4+
Implements credential retrieval for NuGet feeds hosted on Cloudsmith.
5+
"""
6+
7+
import json
8+
import sys
9+
10+
import click
11+
12+
from ....credential_helpers.nuget import get_credentials
13+
14+
15+
@click.command()
16+
@click.argument("uri", required=False, default=None)
17+
def nuget(uri):
18+
"""
19+
NuGet credential helper for Cloudsmith feeds.
20+
21+
Returns credentials in NuGet's expected JSON format:
22+
{"Username": "token", "Password": "...", "Message": ""}
23+
24+
If URI is provided as an argument, uses it directly.
25+
Otherwise reads from stdin.
26+
27+
Examples:
28+
# Direct usage
29+
$ cloudsmith credential-helper nuget https://nuget.cloudsmith.io/org/repo/v3/index.json
30+
{"Username":"token","Password":"eyJ0eXAiOiJKV1Qi...","Message":""}
31+
32+
# Via wrapper (called by NuGet)
33+
$ CredentialProvider.Cloudsmith -uri https://nuget.cloudsmith.io/org/repo/v3/index.json
34+
35+
Environment variables:
36+
CLOUDSMITH_API_KEY: API key for authentication (optional)
37+
CLOUDSMITH_ORG: Organization slug (required for OIDC)
38+
CLOUDSMITH_SERVICE_SLUG: Service account slug (required for OIDC)
39+
"""
40+
try:
41+
if not uri:
42+
uri = sys.stdin.read().strip()
43+
44+
if not uri:
45+
click.echo("Error: No URI provided", err=True)
46+
sys.exit(1)
47+
48+
credentials = get_credentials(uri, debug=False)
49+
50+
if not credentials:
51+
click.echo(
52+
"Error: Unable to retrieve credentials. "
53+
"Set CLOUDSMITH_API_KEY or configure OIDC.",
54+
err=True,
55+
)
56+
sys.exit(1)
57+
58+
click.echo(json.dumps({**credentials, "Message": ""}))
59+
60+
except Exception as e: # pylint: disable=broad-exception-caught
61+
click.echo(f"Error: {e}", err=True)
62+
sys.exit(1)

0 commit comments

Comments
 (0)