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
27 changes: 20 additions & 7 deletions pulp_rust/app/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

from pulp_rust.app.auth import CargoTokenAuthentication
from pulp_rust.app.models import (
RustCargoToken,
RustContent,
RustDistribution,
RustPackageYank,
Expand Down Expand Up @@ -86,6 +87,10 @@ def token_scope_error(token, action, distro):
Scopes only narrow what a token can do, so this runs in addition to the RBAC check
rather than in place of it. An empty scope means the token is not narrowed at all.
"""
if not isinstance(token, RustCargoToken):
# Credentials other than a Cargo token carry no scopes.
return None

if token.actions and action not in token.actions:
return cargo_error(f"this token is not scoped for the {action} action", status=403)

Expand Down Expand Up @@ -322,28 +327,38 @@ 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),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a general change, or do you specifically have in mind a use case for mixing content guards with cargo download?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, I am super busy at the moment, a colleague will be helping on the fixes you asked for.

About the change itself, I added to pulp core the equivalent of https://crates.io/docs/trusted-publishing, it allows CIs to get time and context bound privileges, this helps fight against Arrakis wildlife by ensuring CIs job don't use hardcoded tokens and have limited privileges.

Porting this feature here would duplicate code, so I looked at how other part of pulp (like docker for instance) handle delegating privileges managent to pulp core and replicated it here.

I hope it answers your question :)
Baptiste

}
return HttpResponse(json.dumps(data), content_type="application/json")


class CargoMeApiView(APIView):
class CargoAuthMixin:
"""Try the Cargo token first, then the instance's default authentication classes.

This keeps `cargo login` tokens working while still accepting the other credentials
the deployment is configured for (e.g. HTTP Basic).
"""

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 @@ -353,7 +368,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 @@ -449,12 +463,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
88 changes: 88 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,89 @@ 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


def test_yank_with_basic_auth_succeeds(
gen_user,
rust_distro_api_client,
populated_repo,
):
"""Token scopes only apply to Cargo tokens, so a Basic-authenticated yank goes through."""
alice = gen_user()
rust_distro_api_client.add_role(
populated_repo["distribution"].pulp_href,
{"role": "rust.rustdistribution_publisher", "users": [alice.username]},
)
creds = base64.b64encode(f"{alice.username}:{alice.password}".encode()).decode()

response = cargo_yank(
populated_repo["base_url"], "itoa", "1.0.0", headers={"Authorization": f"Basic {creds}"}
)
assert response.status_code == 200


# --- 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