-
Notifications
You must be signed in to change notification settings - Fork 4
Add per-user Cargo token authentication #40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| Added per-user Cargo token authentication for Cargo API endpoints (publish, yank, unyank, /me), | ||
| replacing the hardcoded stub token. Tokens are created via the REST API and sent by Cargo | ||
| in the `Authorization` header. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| * [Pull-Through Cache](pull-through-cache.md) | ||
| * [Host a Private Registry](private-registry.md) | ||
| * [Authentication & Authorization](authentication.md) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| # Authentication & Authorization | ||
|
|
||
| Pulp Rust uses per-user API tokens for Cargo protocol endpoints (publish, yank, unyank) and | ||
| Pulp's standard RBAC system for controlling access to repositories, remotes, and distributions. | ||
|
|
||
| ## Cargo Token Authentication | ||
|
|
||
| ### Creating a Token | ||
|
|
||
| Create a token via the Pulp REST API using your Pulp credentials: | ||
|
|
||
| ```bash | ||
| http POST http://<pulp-host>/pulp/api/v3/cargo/tokens/ \ | ||
| -a admin:password \ | ||
| name="my-laptop" | ||
| ``` | ||
|
|
||
| The response includes the token value (prefixed with `crg_`). This is shown **once** -- it | ||
| cannot be retrieved again. Store it securely. | ||
|
|
||
| ### Using with Cargo | ||
|
|
||
| Pass the token to Cargo via `cargo login`: | ||
|
|
||
| ```bash | ||
| cargo login --registry my-crates | ||
| # Paste the crg_... token when prompted | ||
| ``` | ||
|
|
||
| Or set it directly in `~/.cargo/credentials.toml`: | ||
|
|
||
| ```toml | ||
| [registries.my-crates] | ||
| token = "crg_..." | ||
| ``` | ||
|
|
||
| Cargo sends the token automatically on state-changing operations (publish, yank, unyank). | ||
| Read-only operations (downloading crates, browsing the index) do not require a token. | ||
|
|
||
| ### Managing Tokens | ||
|
|
||
| ```bash | ||
| # List your tokens (token values are not shown) | ||
| http GET http://<pulp-host>/pulp/api/v3/cargo/tokens/ -a user:password | ||
|
|
||
| # Revoke a token | ||
| http DELETE http://<pulp-host>/pulp/api/v3/cargo/tokens/<token-uuid>/ -a user:password | ||
| ``` | ||
|
|
||
| Users can only see and revoke their own tokens. | ||
|
|
||
| ## Distribution-Scoped Permissions | ||
|
|
||
| Access to publish and yank is controlled per-distribution using Pulp's RBAC system. A user | ||
| needs the appropriate role on a distribution before they can publish or yank crates through it. | ||
|
|
||
| ### Roles | ||
|
|
||
| | Role | Permissions | | ||
| |------|-------------| | ||
| | `rust.rustdistribution_owner` | Full control: view, change, delete, manage roles, publish, yank | | ||
| | `rust.rustdistribution_publisher` | Publish crates and yank/unyank versions | | ||
| | `rust.rustdistribution_viewer` | View the distribution | | ||
|
|
||
| The user who creates a distribution automatically receives the `owner` role on it. | ||
|
|
||
| ### Granting Access | ||
|
|
||
| Grant a user permission to publish to a specific distribution: | ||
|
|
||
| ```bash | ||
| http POST http://<pulp-host>/pulp/api/v3/distributions/rust/rust/<uuid>/add_role/ \ | ||
| -a admin:password \ | ||
| role="rust.rustdistribution_publisher" \ | ||
| users:='["alice"]' | ||
| ``` | ||
|
|
||
| Alice can now publish and yank crates on that distribution using her Cargo token. | ||
|
|
||
| ## Differences from crates.io | ||
|
|
||
| | Feature | crates.io | Pulp Rust | | ||
| |---------|-----------|-----------| | ||
| | Access scope | Per-crate ownership | Per-distribution | | ||
| | Owner management | `cargo owner --add` | Pulp REST API role assignment | | ||
| | Token creation | Web UI at crates.io | Pulp REST API | | ||
| | Per-crate ownership | Yes (user and team owners) | Not supported (planned) | | ||
| | Token scoping | Scoped to endpoints/crates | Not yet supported | | ||
|
|
||
| !!! warning "No per-crate ownership" | ||
| Pulp Rust currently controls access at the distribution level, not per-crate. Any user with | ||
| the `publisher` role on a distribution can publish any crate name to it. Per-crate ownership | ||
| (where only the crate's owner can publish new versions) is planned for a future release. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,37 +1,29 @@ | ||
| """Stub authentication for Cargo API endpoints. | ||
| """Cargo token authentication for Cargo API endpoints.""" | ||
|
|
||
| This is a temporary placeholder — it validates the Authorization header against | ||
| a hardcoded token so that state-changing endpoints (publish, yank, unyank) are | ||
| not completely open. It will be replaced by proper token-based auth later. | ||
| """ | ||
| import hashlib | ||
|
|
||
| import functools | ||
| import json | ||
| from django.utils import timezone | ||
| from rest_framework.authentication import BaseAuthentication | ||
| from rest_framework.exceptions import AuthenticationFailed | ||
|
|
||
| from django.http import HttpResponse | ||
| from pulp_rust.app.models import RustCargoToken | ||
|
|
||
| STUB_TOKEN = "i_understand_that_pulp_rust_does_not_support_proper_auth_yet" | ||
|
|
||
| class CargoTokenAuthentication(BaseAuthentication): | ||
| """Authenticate Cargo requests via the Authorization header token.""" | ||
|
|
||
| def require_cargo_token(view_method): | ||
| """Decorator that validates the Cargo Authorization header against the stub token. | ||
|
|
||
| Returns a 403 with a Cargo-style JSON error if the token is missing or incorrect. | ||
| """ | ||
|
|
||
| @functools.wraps(view_method) | ||
| def wrapper(self, request, *args, **kwargs): | ||
| def authenticate(self, request): | ||
| token = request.META.get("HTTP_AUTHORIZATION") | ||
| if token == STUB_TOKEN: | ||
| return view_method(self, request, *args, **kwargs) | ||
| if not token: | ||
| detail = "this endpoint requires an authorization token" | ||
| else: | ||
| detail = "invalid authorization token" | ||
| return HttpResponse( | ||
| json.dumps({"errors": [{"detail": detail}]}), | ||
| content_type="application/json", | ||
| status=403, | ||
| ) | ||
|
|
||
| return wrapper | ||
| if not token or not token.startswith("crg_"): | ||
| return None | ||
| token_hash = hashlib.sha256(token.encode()).hexdigest() | ||
| try: | ||
| cargo_token = RustCargoToken.objects.select_related("user").get(token_hash=token_hash) | ||
| except RustCargoToken.DoesNotExist: | ||
| raise AuthenticationFailed("invalid cargo token") | ||
| cargo_token.last_used = timezone.now() | ||
| cargo_token.save(update_fields=["last_used"]) | ||
| return (cargo_token.user, cargo_token) | ||
|
|
||
| def authenticate_header(self, request): | ||
| return "CargoToken" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # Generated by Django 5.2.14 on 2026-07-07 12:33 | ||
|
|
||
| import django.db.models.deletion | ||
| import django_lifecycle.mixins | ||
| import pulpcore.app.models.base | ||
| from django.conf import settings | ||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ('rust', '0002_add_rbac'), | ||
| migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.CreateModel( | ||
| name='RustCargoToken', | ||
| fields=[ | ||
| ('pulp_id', models.UUIDField(default=pulpcore.app.models.base.pulp_uuid, editable=False, primary_key=True, serialize=False)), | ||
| ('pulp_created', models.DateTimeField(auto_now_add=True)), | ||
| ('pulp_last_updated', models.DateTimeField(auto_now=True, null=True)), | ||
| ('name', models.CharField(max_length=255)), | ||
| ('token_hash', models.CharField(db_index=True, max_length=64, unique=True)), | ||
| ('last_used', models.DateTimeField(blank=True, null=True)), | ||
| ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cargo_tokens', to=settings.AUTH_USER_MODEL)), | ||
| ], | ||
| options={ | ||
| 'abstract': False, | ||
| }, | ||
| bases=(django_lifecycle.mixins.LifecycleModelMixin, models.Model), | ||
| ), | ||
| ] |
21 changes: 21 additions & 0 deletions
21
pulp_rust/app/migrations/0004_alter_rustcargotoken_options_and_more.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| # Generated by Django 5.2.14 on 2026-07-13 08:54 | ||
|
|
||
| from django.db import migrations | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
|
|
||
| dependencies = [ | ||
| ('rust', '0003_rustcargotoken'), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.AlterModelOptions( | ||
| name='rustcargotoken', | ||
| options={'default_related_name': '%(app_label)s_%(model_name)s'}, | ||
| ), | ||
| migrations.AlterModelOptions( | ||
| name='rustdistribution', | ||
| options={'default_related_name': '%(app_label)s_%(model_name)s', 'permissions': [('manage_roles_rustdistribution', 'Can manage roles on rust distributions'), ('publish_rustdistribution', 'Can publish crates to this distribution'), ('yank_rustdistribution', 'Can yank/unyank crates in this distribution')]}, | ||
| ), | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't displaying the token values to any API user, right? Even the user that created it should probably not be able to see it again after initial creation.
It also doesn't map directly to any field name
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, in this implementation there is no way to retrieve token value. It's shown just once, right after creation in the response. So this field exists only for this one response. Any reads on this field will return None.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So you're saying that the token value is filled out on creation, but afterwards (list, detail, etc.) it shows as
None?Do you think it would be better to use two serializers so that the field is ommitted entirely in the post-creation lookup cases, rather than set null?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I guess you are right, that makes more sense