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
26 changes: 20 additions & 6 deletions cloudsmith_cli/cli/tests/test_credential_helper_cargo.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ def test_get_returns_token_for_cloudsmith_registry(credential):
assert messages[1] == {
"Ok": {
"kind": "get",
"token": "k_abc",
"token": "token k_abc",
"cache": "session",
"operation_independent": True,
}
Expand All @@ -127,7 +127,7 @@ def test_get_serves_every_operation(operation, credential):
request = _request(operation=operation, name="sample", vers="0.1.0")
_, _, messages = _session(request, credential=credential)

assert messages[1]["Ok"]["token"] == "k_abc"
assert messages[1]["Ok"]["token"] == "token k_abc"


def test_get_uses_the_cargo_backend_kind_for_custom_domains(credential):
Expand All @@ -149,10 +149,24 @@ def test_get_uses_the_cargo_backend_kind_for_custom_domains(credential):
def test_index_url_keeps_its_sparse_prefix_out_of_the_host_match(credential):
"""Cargo's `sparse+` prefix and the repo path don't defeat the host check."""
assert (
get_credentials(CLOUDSMITH_INDEX, credential=credential, org="acme") == "k_abc"
get_credentials(CLOUDSMITH_INDEX, credential=credential, org="acme")
== "token k_abc"
)


def test_get_returns_bearer_scheme_for_sso_credential():
"""An SSO credential is returned with the Bearer authorization scheme."""
credential = CredentialResult(
api_key="jwt_token",
source_name="keyring",
auth_type="bearer",
)

_, _, messages = _session(_request(), credential=credential)

assert messages[1]["Ok"]["token"] == "Bearer jwt_token"


# ---------------------------------------------------------------------------
# 3. get — the refusal paths, which Cargo distinguishes
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -247,7 +261,7 @@ def test_malformed_request_lines_are_answered_not_crashed(line, credential):
assert (code, stderr) == (0, None)
assert messages[1]["Err"]["kind"] == "other"
# The following well-formed request is still served.
assert messages[2]["Ok"]["token"] == "k_abc"
assert messages[2]["Ok"]["token"] == "token k_abc"


def test_blank_lines_are_skipped(credential):
Expand Down Expand Up @@ -322,7 +336,7 @@ def test_cli_speaks_the_protocol_on_stdin_and_stdout(runner):
messages = [json.loads(line) for line in result.stdout.splitlines()]
assert result.exit_code == 0
assert messages[0] == hello()
assert messages[1]["Ok"]["token"] == "k_abc"
assert messages[1]["Ok"]["token"] == "token k_abc"


def test_cli_accepts_the_cargo_plugin_flag_and_extra_provider_args(runner):
Expand All @@ -336,7 +350,7 @@ def test_cli_accepts_the_cargo_plugin_flag_and_extra_provider_args(runner):

messages = [json.loads(line) for line in result.stdout.splitlines()]
assert result.exit_code == 0
assert messages[1]["Ok"]["token"] == "k_abc"
assert messages[1]["Ok"]["token"] == "token k_abc"


def test_cli_exits_non_zero_with_a_hint_when_no_credential_resolves(runner):
Expand Down
2 changes: 2 additions & 0 deletions cloudsmith_cli/core/credentials/providers/oidc_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def resolve( # pylint: disable=too-many-return-statements
api_key=cached,
source_name="oidc",
source_detail=f"OIDC [cached] (org: {org}, service: {service_slug})",
auth_type="bearer",
)

from ..oidc.detectors import detect_environment
Expand Down Expand Up @@ -112,4 +113,5 @@ def resolve( # pylint: disable=too-many-return-statements
api_key=cloudsmith_token,
source_name="oidc",
source_detail=f"OIDC via {detector.name} (org: {org}, service: {service_slug})",
auth_type="bearer",
)
54 changes: 54 additions & 0 deletions cloudsmith_cli/core/tests/test_oidc_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Tests for the OIDC credential provider."""

from unittest.mock import Mock, patch

from cloudsmith_cli.core.credentials.models import CredentialContext
from cloudsmith_cli.core.credentials.providers.oidc_provider import OidcProvider


def _context() -> CredentialContext:
return CredentialContext(
org="cloudsmith",
oidc_service_slug="github-actions",
)


def test_cached_oidc_token_is_a_bearer_credential():
"""Cached exchanged tokens retain their HTTP authorization scheme."""
with patch(
"cloudsmith_cli.core.credentials.oidc.cache.get_cached_token",
return_value="cached-token",
):
credential = OidcProvider().resolve(_context())

assert credential is not None
assert credential.auth_type == "bearer"


def test_exchanged_oidc_token_is_a_bearer_credential():
"""Freshly exchanged tokens use Bearer authentication."""
detector = Mock(name="github-actions")
detector.name = "github-actions"
detector.get_token.return_value = "vendor-token"

with (
patch(
"cloudsmith_cli.core.credentials.oidc.cache.get_cached_token",
return_value=None,
),
patch(
"cloudsmith_cli.core.credentials.oidc.cache.store_cached_token",
),
patch(
"cloudsmith_cli.core.credentials.oidc.detectors.detect_environment",
return_value=detector,
),
patch(
"cloudsmith_cli.core.credentials.oidc.exchange.exchange_oidc_token",
return_value="exchanged-token",
),
):
credential = OidcProvider().resolve(_context())

assert credential is not None
assert credential.auth_type == "bearer"
18 changes: 15 additions & 3 deletions cloudsmith_cli/credential_helpers/cargo/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ def get_credentials(index_url, credential=None, api_host=None, org=None):
org: Organisation slug whose custom domains to match against

Returns:
str: The token Cargo sends as its ``Authorization`` header, or None
str: The complete value Cargo sends as its ``Authorization`` header,
or None
"""
if not credential or not credential.api_key:
return None
Expand All @@ -109,7 +110,18 @@ def get_credentials(index_url, credential=None, api_host=None, org=None):
):
return None

return credential.api_key
return _authorization_value(credential)


def _authorization_value(credential) -> str:
"""Return the complete HTTP Authorization value for *credential*.

Cargo forwards the credential provider's ``token`` field verbatim as the
Authorization header. Cloudsmith API credentials use the ``token`` scheme,
while SSO credentials use ``Bearer``.
"""
scheme = "Bearer" if credential.auth_type == "bearer" else "token"
return f"{scheme} {credential.api_key}"


def _handle_get(request, credential, api_host, org) -> dict:
Expand Down Expand Up @@ -137,7 +149,7 @@ def _handle_get(request, credential, api_host, org) -> dict:
return _ok(
{
"kind": "get",
"token": credential.api_key,
"token": _authorization_value(credential),
# A Cloudsmith token is organisation-wide and not scoped to the
# read/publish/yank/owners operation, so it is cacheable for the
# session and across operations.
Expand Down
Loading