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
25 changes: 18 additions & 7 deletions pulp_rust/app/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,28 +306,41 @@ def retrieve(self, request, repo):
data = {
"dl": self.base_download_url,
"api": self.base_api_url,
"auth-required": False,
"auth-required": bool(self.distribution.content_guard_id),
}
return HttpResponse(json.dumps(data), content_type="application/json")


class CargoMeApiView(APIView):
class CargoAuthMixin:
"""Authenticate the Cargo token first, then the deployment's configured authenticators.

Cargo endpoints must not shadow ``DEFAULT_AUTHENTICATION_CLASSES``: hardcoding only
``CargoTokenAuthentication`` locks them to ``crg_`` tokens and rejects every other
credential the instance accepts (Basic, session, SSO, OIDC/workload identity).
Prepending the Cargo token keeps ``cargo login`` working while letting any configured
authenticator (e.g. a CI bearer token) reach the view.
"""

def get_authenticators(self):
return [CargoTokenAuthentication(), *super().get_authenticators()]


class CargoMeApiView(CargoAuthMixin, APIView):
"""
Auth verification endpoint for ``cargo login``.

Cargo calls GET /me after login to verify the token is valid.
See: https://doc.rust-lang.org/cargo/reference/registry-web-api.html
"""

authentication_classes = [CargoTokenAuthentication]
permission_classes = [IsAuthenticated]
renderer_classes = [JSONRenderer]

def get(self, request, **kwargs):
return HttpResponse(json.dumps({"ok": True}), content_type="application/json")


class CargoPublishApiView(APIView):
class CargoPublishApiView(CargoAuthMixin, APIView):
"""
View for Cargo's crate publish endpoint (PUT /api/v1/crates/new).

Expand All @@ -337,7 +350,6 @@ class CargoPublishApiView(APIView):
See: https://doc.rust-lang.org/cargo/reference/registry-web-api.html#publish
"""

authentication_classes = [CargoTokenAuthentication]
permission_classes = [IsAuthenticated]
renderer_classes = [JSONRenderer]

Expand Down Expand Up @@ -430,12 +442,11 @@ def put(self, request, **kwargs):
)


class CargoDownloadApiView(APIView):
class CargoDownloadApiView(CargoAuthMixin, APIView):
"""
View for Cargo's crate download, readme, yank, and unyank endpoints.
"""

authentication_classes = [CargoTokenAuthentication]
renderer_classes = [PlainTextRenderer, JSONRenderer]

def get_permissions(self):
Expand Down
69 changes: 69 additions & 0 deletions pulp_rust/tests/functional/api/test_auth.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Tests for Cargo token authentication on Cargo API endpoints."""

import base64
import uuid
from urllib.parse import urljoin

from pulp_rust.tests.functional.utils import (
Expand Down Expand Up @@ -267,3 +269,70 @@ def test_yank_requires_distribution_permission(
# Now Alice can yank
response = cargo_yank(base_url, "itoa", "1.0.0", headers=headers)
assert response.status_code == 200


# --- Non-Cargo-token authenticators (the instance's default auth stack) ---


def test_me_with_basic_auth_succeeds(
rust_repo_factory,
rust_distribution_factory,
cargo_registry_url,
gen_user,
):
"""A non-Cargo credential (HTTP Basic) must authenticate too.

The Cargo endpoints prepend the Cargo token to the instance's configured
authenticators instead of replacing them, so any deployment auth reaches them.
"""
repository = rust_repo_factory()
distribution = rust_distribution_factory(repository=repository.pulp_href)
base = cargo_registry_url(distribution.base_path)

user = gen_user()
creds = base64.b64encode(f"{user.username}:{user.password}".encode()).decode()

response = cargo_api_request(
"GET", urljoin(base, "me"), headers={"Authorization": f"Basic {creds}"}
)
assert response.status_code == 200
assert response.json()["ok"] is True


# --- config.json auth-required reflects the content guard ---


def test_config_json_auth_required_false_without_guard(
rust_repo_factory,
rust_distribution_factory,
cargo_registry_url,
):
"""An unguarded distribution advertises auth-required: false."""
repository = rust_repo_factory()
distribution = rust_distribution_factory(repository=repository.pulp_href)
config_url = urljoin(cargo_registry_url(distribution.base_path), "config.json")

response = cargo_api_request("GET", config_url)
assert response.json()["auth-required"] is False


def test_config_json_auth_required_true_with_guard(
rust_repo_factory,
rust_distribution_factory,
cargo_registry_url,
pulpcore_bindings,
gen_object_with_cleanup,
):
"""A guarded distribution advertises auth-required: true, so cargo sends its
credentials on the index and downloads, not only on the write API."""
guard = gen_object_with_cleanup(
pulpcore_bindings.ContentguardsRbacApi, {"name": str(uuid.uuid4())}
)
repository = rust_repo_factory()
distribution = rust_distribution_factory(
repository=repository.pulp_href, content_guard=guard.pulp_href
)
config_url = urljoin(cargo_registry_url(distribution.base_path), "config.json")

response = cargo_api_request("GET", config_url)
assert response.json()["auth-required"] is True
Loading