Skip to content
Open
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
31 changes: 31 additions & 0 deletions cloudsmith_cli/cli/commands/credential_helper/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""
Credential helper commands for Cloudsmith.

This module provides credential helper commands for package managers
that follow their respective credential helper protocols.
"""

import click

from ..main import main
from .docker import docker as docker_cmd


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

These commands provide credentials for package managers like Docker.
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
"""


credential_helper.add_command(docker_cmd, name="docker")

main.add_command(credential_helper, name="credential-helper")
80 changes: 80 additions & 0 deletions cloudsmith_cli/cli/commands/credential_helper/docker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""
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
from ...decorators import common_api_auth_options, resolve_credentials


@click.command()
@common_api_auth_options
@resolve_credentials
def docker(opts):
"""
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 custom domain support)
"""
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,
credential=opts.credential,
session=opts.session,
api_host=opts.api_host or "https://api.cloudsmith.io",
)

if not credentials:
click.echo(
"Error: Unable to retrieve credentials. "
"Make sure you have a valid cloudsmith-cli session, "
"this can be checked with `cloudsmith whoami`.",
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)
6 changes: 6 additions & 0 deletions cloudsmith_cli/credential_helpers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""
Credential helpers for various package managers.

This package provides credential helper implementations for Docker, pip, npm, etc.
Each helper follows its respective package manager's credential helper protocol.
"""
86 changes: 86 additions & 0 deletions cloudsmith_cli/credential_helpers/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""
Shared utilities for credential helpers.

Provides domain checking used by all credential helpers.
"""

import logging
import os

logger = logging.getLogger(__name__)


def extract_hostname(url):
"""
Extract bare hostname from any URL format.

Handles protocols, sparse+ prefix, ports, paths, and trailing slashes.

Args:
url: URL in any format (e.g., "sparse+https://cargo.cloudsmith.io/org/repo/")

Returns:
str: Lowercase hostname (e.g., "cargo.cloudsmith.io")
"""
if not url:
return ""

normalized = url.lower().strip()

# Remove sparse+ prefix (Cargo)
if normalized.startswith("sparse+"):
normalized = normalized[7:]

# Remove protocol
if "://" in normalized:
normalized = normalized.split("://", 1)[1]

# Remove userinfo (user@host)
if "@" in normalized.split("/")[0]:
normalized = normalized.split("@", 1)[1]

# Extract hostname (before first / or :)
hostname = normalized.split("/")[0].split(":")[0]

return hostname


def is_cloudsmith_domain(url, session=None, api_key=None, api_host=None):
"""
Check if a URL points to a Cloudsmith service.

Checks standard *.cloudsmith.io domains first (no auth needed).
If not a standard domain, queries the Cloudsmith API for custom domains.

Args:
url: URL or hostname to check
session: Pre-configured requests.Session with proxy/SSL settings
api_key: API key for authenticating custom domain lookups
api_host: Cloudsmith API host URL

Returns:
bool: True if this is a Cloudsmith domain
"""
hostname = extract_hostname(url)
if not hostname:
return False

# Standard Cloudsmith domains — no auth needed
if hostname.endswith("cloudsmith.io") or hostname == "cloudsmith.io":
return True

# Custom domains require org + auth
org = os.environ.get("CLOUDSMITH_ORG", "").strip()
if not org:
return False

if not api_key:
return False

from .custom_domains import get_custom_domains_for_org

custom_domains = get_custom_domains_for_org(
org, session=session, api_key=api_key, api_host=api_host
)

return hostname in [d.lower() for d in custom_domains]
Loading
Loading