diff --git a/pulp_rust/app/views.py b/pulp_rust/app/views.py index 10ead26..ba20ed4 100644 --- a/pulp_rust/app/views.py +++ b/pulp_rust/app/views.py @@ -306,12 +306,26 @@ 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``. @@ -319,7 +333,6 @@ class CargoMeApiView(APIView): See: https://doc.rust-lang.org/cargo/reference/registry-web-api.html """ - authentication_classes = [CargoTokenAuthentication] permission_classes = [IsAuthenticated] renderer_classes = [JSONRenderer] @@ -327,7 +340,7 @@ 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). @@ -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] @@ -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): diff --git a/pulp_rust/tests/functional/api/test_auth.py b/pulp_rust/tests/functional/api/test_auth.py index b75ebdf..95ed2ac 100644 --- a/pulp_rust/tests/functional/api/test_auth.py +++ b/pulp_rust/tests/functional/api/test_auth.py @@ -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 ( @@ -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