Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cloudsmith_cli/cli/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
auth,
check,
copy,
credential_helper,
delete,
dependencies,
docs,
Expand Down
46 changes: 46 additions & 0 deletions cloudsmith_cli/cli/commands/credential_helper/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""
Credential helper commands for Cloudsmith.

This module provides credential helper commands for various package managers
(Docker, pip, npm, etc.) that follow their respective credential helper protocols.
"""

import click

from ..main import main
from .cargo import cargo as cargo_cmd
from .docker import docker as docker_cmd
from .npm import npm as npm_cmd
from .nuget import nuget as nuget_cmd
from .terraform import terraform as terraform_cmd


@click.group()
def credential_helper():
"""
Credential helpers for package managers.

These commands provide credentials for package managers like Docker, pip,
npm, Terraform, Cargo, and NuGet. They are typically called by
wrapper binaries (e.g., docker-credential-cloudsmith) or used directly
for debugging.

Examples:
# Test Docker credential helper
$ echo "docker.cloudsmith.io" | cloudsmith credential-helper docker

# Test Terraform credential helper
$ cloudsmith credential-helper terraform terraform.cloudsmith.io

# Test npm/pnpm token helper
$ cloudsmith credential-helper npm
"""


credential_helper.add_command(cargo_cmd, name="cargo")
credential_helper.add_command(docker_cmd, name="docker")
credential_helper.add_command(npm_cmd, name="npm")
credential_helper.add_command(nuget_cmd, name="nuget")
credential_helper.add_command(terraform_cmd, name="terraform")

main.add_command(credential_helper, name="credential-helper")
146 changes: 146 additions & 0 deletions cloudsmith_cli/cli/commands/credential_helper/cargo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""
Cargo credential helper command.

Implements credential retrieval for Cargo registries hosted on Cloudsmith.

See: https://doc.rust-lang.org/cargo/reference/credential-provider-protocol.html
"""

import json
import sys

import click

from ....credential_helpers.cargo import get_credentials


@click.command()
@click.option(
"--cargo-plugin",
is_flag=True,
default=False,
help="Run in Cargo credential provider plugin mode (JSON-line protocol).",
)
@click.argument("index_url", required=False, default=None)
def cargo(cargo_plugin, index_url):
"""
Cargo credential helper for Cloudsmith registries.

Returns credentials as a Bearer token for Cargo sparse registries.

If --cargo-plugin is passed, runs the full Cargo credential provider
JSON-line protocol (hello handshake, request/response). This is used
by the cargo-credential-cloudsmith wrapper binary.

If INDEX_URL is provided as an argument, uses it directly.
Otherwise reads from stdin.

Examples:
# Direct usage
$ cloudsmith credential-helper cargo sparse+https://cargo.cloudsmith.io/org/repo/
Bearer eyJ0eXAiOiJKV1Qi...

# Via wrapper (called by Cargo)
$ cargo-credential-cloudsmith --cargo-plugin

Environment variables:
CLOUDSMITH_API_KEY: API key for authentication (optional)
CLOUDSMITH_ORG: Organization slug (required for OIDC)
CLOUDSMITH_SERVICE_SLUG: Service account slug (required for OIDC)
"""
if cargo_plugin:
_run_cargo_plugin()
return

try:
if not index_url:
index_url = sys.stdin.read().strip()

if not index_url:
click.echo("Error: No index URL provided", err=True)
sys.exit(1)

token = get_credentials(index_url, debug=False)

if not token:
click.echo(
"Error: Unable to retrieve credentials. "
"Set CLOUDSMITH_API_KEY or configure OIDC.",
err=True,
)
sys.exit(1)

click.echo(json.dumps({"token": f"Bearer {token}"}))

except Exception as e: # pylint: disable=broad-exception-caught
click.echo(f"Error: {e}", err=True)
sys.exit(1)


def _run_cargo_plugin():
"""
Run the full Cargo credential provider JSON-line protocol.

See: https://doc.rust-lang.org/cargo/reference/credential-provider-protocol.html
"""
hello = {"v": [1]}
sys.stdout.write(json.dumps(hello) + "\n")
sys.stdout.flush()

try:
line = sys.stdin.readline()
if not line:
sys.exit(0)

request = json.loads(line)
except (json.JSONDecodeError, EOFError):
sys.exit(1)

kind = request.get("kind", "")
registry = request.get("registry", {})
index_url = registry.get("index-url", "")

if kind == "get":
_handle_get(index_url)
elif kind in ("login", "logout"):
_handle_unsupported(kind)
else:
_write_error("operation-not-supported", f"Unknown operation: {kind}")


def _handle_get(index_url):
"""Handle a 'get' request from Cargo."""
token = get_credentials(index_url)
if not token:
_write_error(
"not-found",
"No credentials available. Set CLOUDSMITH_API_KEY or configure OIDC.",
)
return

response = {
"Ok": {
"kind": "get",
"token": f"Bearer {token}",
"cache": "session",
"operation_independent": True,
}
}
sys.stdout.write(json.dumps(response) + "\n")
sys.stdout.flush()


def _handle_unsupported(kind):
"""Handle unsupported operations (login/logout)."""
_write_error(
"operation-not-supported",
f"Operation '{kind}' is not supported. "
"Credentials are managed by the Cloudsmith credential chain.",
)


def _write_error(kind, message):
"""Write an error response in Cargo JSON-line format."""
response = {"Err": {"kind": kind, "message": message}}
sys.stdout.write(json.dumps(response) + "\n")
sys.stdout.flush()
73 changes: 73 additions & 0 deletions cloudsmith_cli/cli/commands/credential_helper/docker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""
Docker credential helper command.

Implements the Docker credential helper protocol for Cloudsmith registries.

See: https://github.com/docker/docker-credential-helpers
"""

import json
import sys

import click

from ....credential_helpers.docker import get_credentials


@click.command()
def docker():
"""
Docker credential helper for Cloudsmith registries.

Reads a Docker registry server URL from stdin and returns credentials in JSON format.
This command implements the 'get' operation of the Docker credential helper protocol.

Only provides credentials for Cloudsmith Docker registries (docker.cloudsmith.io).

Input (stdin):
Server URL as plain text (e.g., "docker.cloudsmith.io")

Output (stdout):
JSON: {"Username": "token", "Secret": "<cloudsmith-token>"}

Exit codes:
0: Success
1: Error (no credentials available, not a Cloudsmith registry, etc.)

Examples:
# Manual testing
$ echo "docker.cloudsmith.io" | cloudsmith credential-helper docker
{"Username":"token","Secret":"eyJ0eXAiOiJKV1Qi..."}

# Called by Docker via wrapper
$ echo "docker.cloudsmith.io" | docker-credential-cloudsmith get
{"Username":"token","Secret":"eyJ0eXAiOiJKV1Qi..."}

Environment variables:
CLOUDSMITH_API_KEY: API key for authentication (optional)
CLOUDSMITH_ORG: Organization slug (required for OIDC)
CLOUDSMITH_SERVICE_SLUG: Service account slug (required for OIDC)
"""
try:
server_url = sys.stdin.read().strip()

if not server_url:
click.echo("Error: No server URL provided on stdin", err=True)
sys.exit(1)

credentials = get_credentials(server_url, debug=False)

if not credentials:
click.echo(
"Error: Unable to retrieve credentials. "
"Make sure you have either CLOUDSMITH_API_KEY set, "
"or CLOUDSMITH_ORG + CLOUDSMITH_SERVICE_SLUG for OIDC authentication.",
err=True,
)
sys.exit(1)

click.echo(json.dumps(credentials))

except Exception as e: # pylint: disable=broad-exception-caught
click.echo(f"Error: {e}", err=True)
sys.exit(1)
58 changes: 58 additions & 0 deletions cloudsmith_cli/cli/commands/credential_helper/npm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""
npm/pnpm token helper command.

Prints a raw Cloudsmith API token for use with pnpm's tokenHelper.

See: https://pnpm.io/npmrc#url-tokenhelper
"""

import sys

import click

from ....credential_helpers.npm import get_token


@click.command()
def npm():
"""
npm/pnpm token helper for Cloudsmith registries.

Prints a raw API token to stdout for use with pnpm's tokenHelper configuration.

Examples:
# Direct usage
$ cloudsmith credential-helper npm
eyJ0eXAiOiJKV1Qi...

# Via wrapper (called by pnpm)
$ npm-credentials-cloudsmith
eyJ0eXAiOiJKV1Qi...

Configuration in ~/.npmrc:
//npm.cloudsmith.io/:tokenHelper=/absolute/path/to/npm-credentials-cloudsmith

Find the path with: which npm-credentials-cloudsmith

Environment variables:
CLOUDSMITH_API_KEY: API key for authentication (optional)
CLOUDSMITH_ORG: Organization slug (required for OIDC)
CLOUDSMITH_SERVICE_SLUG: Service account slug (required for OIDC)
"""
try:
token = get_token(debug=False)

if not token:
click.echo(
"Error: Unable to retrieve credentials. "
"Set CLOUDSMITH_API_KEY or configure OIDC.",
err=True,
)
sys.exit(1)

sys.stdout.write(token)
sys.stdout.flush()

except Exception as e: # pylint: disable=broad-exception-caught
click.echo(f"Error: {e}", err=True)
sys.exit(1)
64 changes: 64 additions & 0 deletions cloudsmith_cli/cli/commands/credential_helper/nuget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""
NuGet credential helper command.

Implements credential retrieval for NuGet feeds hosted on Cloudsmith.

See: https://learn.microsoft.com/en-us/nuget/reference/extensibility/nuget-exe-credential-providers
"""

import json
import sys

import click

from ....credential_helpers.nuget import get_credentials


@click.command()
@click.argument("uri", required=False, default=None)
def nuget(uri):
"""
NuGet credential helper for Cloudsmith feeds.

Returns credentials in NuGet's expected JSON format:
{"Username": "token", "Password": "...", "Message": ""}

If URI is provided as an argument, uses it directly.
Otherwise reads from stdin.

Examples:
# Direct usage
$ cloudsmith credential-helper nuget https://nuget.cloudsmith.io/org/repo/v3/index.json
{"Username":"token","Password":"eyJ0eXAiOiJKV1Qi...","Message":""}

# Via wrapper (called by NuGet)
$ CredentialProvider.Cloudsmith -uri https://nuget.cloudsmith.io/org/repo/v3/index.json

Environment variables:
CLOUDSMITH_API_KEY: API key for authentication (optional)
CLOUDSMITH_ORG: Organization slug (required for OIDC)
CLOUDSMITH_SERVICE_SLUG: Service account slug (required for OIDC)
"""
try:
if not uri:
uri = sys.stdin.read().strip()

if not uri:
click.echo("Error: No URI provided", err=True)
sys.exit(1)

credentials = get_credentials(uri, debug=False)

if not credentials:
click.echo(
"Error: Unable to retrieve credentials. "
"Set CLOUDSMITH_API_KEY or configure OIDC.",
err=True,
)
sys.exit(1)

click.echo(json.dumps({**credentials, "Message": ""}))

except Exception as e: # pylint: disable=broad-exception-caught
click.echo(f"Error: {e}", err=True)
sys.exit(1)
Loading