Skip to content

feat: enable RBAC by making PulpServiceAccessPolicy the default - #1535

Merged
dkliban merged 1 commit into
pulp:mainfrom
CryptoRodeo:feat/enable-rbac
Sep 30, 2026
Merged

dkliban merged 1 commit into
pulp:mainfrom
CryptoRodeo:feat/enable-rbac

Conversation

@CryptoRodeo

@CryptoRodeo CryptoRodeo commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

This flips the default permission class from the old DomainBasedPermission to PulpServiceAccessPolicy, so RBAC is actually the thing enforcing access now instead of the org_id/DomainOrg checks. The settings change lives in both dev-container/settings.py and deploy/clowdapp.yaml; the code side updates the few viewsets that hard-coded the old class:

  • PyPIYankMonitorViewSet drops DomainBasedPermission.
  • CreateDomainView / MigrateDomainView move to [IsAuthenticated] plus explicit checks (set_domain_create_context for create, has_perm("core.change_domain") for migrate) so they don't lean on the old permission class implicitly.

The bulk of the diff is test reconciliation. A pile of RBAC-focused functional tests had been merged to main but couldn't run for real until RBAC was on, so they're restored here, plus the existing suite is updated where it asserted the old permission model's behavior. The one thing worth calling out: under RBAC, "denied" doesn't always mean 403.

  • Repository listing lets any authenticated user in and scopes the queryset by role, so a caller with no role on the domain gets an empty 200 (no leak), not a 403. Those assertions changed to 200 + count == 0.
  • Content listing is different: ACCESS_POLICIES["content/file/files"] gates the list action on has_domain_perms:core.view_content, so a caller without that permission is denied outright with a 403. Those assertions stay 403.
  • Non-public domains now get a default identity content guard, so anonymous GETs against them are denied. The one auth test that assumed an unguarded domain now uses a public- domain (the only unguarded case).

The main thing I set out to protect: the orphaned-content fix still works under RBAC. When you upload content that isn't in any repository yet, the domain owner still has to be able to read it back. PulpServiceAccessPolicy.scope_queryset handles this first (via _is_domain_content_read) before any of the other read-grant branches, so a domain member holding core.view_content gets the content queryset back unscoped. Verified end-to-end: test_content_view_after_upload.py passes 3/3 in the dev container (owner reads orphan content via both the generic and typed endpoints; an unrelated org is denied with 403).

One tenant-isolation guard: scope_queryset's per-domain read grants (DOMAIN_ACCESS_POLICIES subscription/readonly-group) only return a queryset unscoped for models base.py already domain-scoped (those with a pulp_domain FK). Global models (Domain, Group, User, Role) fall through instead -- Domain to its dedicated block that shows only the policy domain + public-*. Without this a Lightwell-ReadOnly member hitting GET /api/pulp/lightwell/api/v3/domains/ would have listed every tenant's domain. admin-readonly stays a global read by design.

Summary by Sourcery

Make RBAC the default authorization model and restore equivalent domain and content access while enforcing tenant-safe queryset scoping.

New Features:

  • Enable PulpServiceAccessPolicy as the default authorization mechanism for REST APIs.
  • Restore RBAC access for public domains, orphan content, organization members, and read-only policy groups while preserving tenant isolation.

Bug Fixes:

  • Ensure domain creation consistently assigns DomainOrg records and RBAC roles, including requests without internal organization identifiers and generic domain endpoint requests.
  • Prevent failed domain creation requests from leaking authorization context between requests.
  • Ensure service roles include permissions from all plugins during migrations.

Enhancements:

  • Replace legacy permission-class dependencies on viewsets with authenticated access and explicit object-level authorization where required.
  • Align queryset scoping with RBAC grants so allowed reads are visible without exposing unrelated tenant data.

Build:

  • Remove the obsolete container registry re-rooting patch from the image build.

Deployment:

  • Update development and deployment configuration to use PulpServiceAccessPolicy by default.

Documentation:

  • Document the RBAC authorization switchover and its restored access behavior.

Tests:

  • Restore and update functional coverage for RBAC domain, content, public-domain, organization, group, and read-only access scenarios.
  • Add regression coverage for orphan-content reads, organization-role backfills, domain creation context, public-domain reads, and service-role population.

Chores:

  • Retain DomainBasedPermission only for the legacy registry patch path pending its removal in a follow-up.

@sourcery-ai

sourcery-ai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This PR switches the service from DomainBasedPermission to PulpServiceAccessPolicy by default, updates endpoint-specific authorization and safe context handling, extends policy queryset scoping for RBAC grants without tenant leaks, and reconciles the functional suite around the resulting 200/403/404 semantics and restored domain/content access.

Sequence diagram for RBAC domain creation

sequenceDiagram
    actor User
    participant API as CreateDomainView
    participant Context as authorization context
    participant Signal as post_create_domain signal
    participant DB as Domain database

    User->>API: POST domain
    API->>API: IsAuthenticated
    API->>Context: set_domain_create_context(request)
    API->>DB: serializer.save()
    DB->>Signal: post_create_domain
    Signal->>Context: consume group, org_id, user_id
    DB-->>API: created domain
    API-->>User: 201 Created
    API->>Context: clear context in finally
Loading

Sequence diagram for RBAC content read and queryset scoping

sequenceDiagram
    actor User
    participant API as Content endpoint
    participant Policy as PulpServiceAccessPolicy
    participant Scope as scope_queryset
    participant DB as Content database

    User->>API: GET content
    API->>Policy: has_permission
    Policy->>Policy: _is_domain_content_read
    alt domain member has core.view_content
        Policy-->>API: allow
        API->>Scope: scope_queryset
        Scope-->>API: return domain content queryset
        API->>DB: fetch orphan and repository content
        DB-->>User: 200 content
    else missing content permission
        Policy-->>API: deny
        API-->>User: 403 Forbidden
    end
Loading

Flow diagram for tenant-safe domain listing

flowchart TD
    Start[Authenticated domain listing] --> Policy[PulpServiceAccessPolicy scope_queryset]
    Policy --> Admin{Admin-readonly member?}
    Admin -->|Yes| All[Return global queryset]
    Admin -->|No| Domain[Apply domain and public-domain rules]
    Domain --> Role{Readonly policy group?}
    Role -->|Yes| Allowed[Include policy domain and public-* domains]
    Role -->|No| RBAC[Apply standard RBAC object scoping]
    Allowed --> Result[Return tenant-safe results]
    RBAC --> Result
    All --> Result
Loading

File-Level Changes

Change Details Files
Make RBAC the active default authorization model across development and deployment configurations.
  • Replace DomainBasedPermission with PulpServiceAccessPolicy in REST framework defaults.
  • Remove view-level overrides that bypass the default policy for content-view and PyPI yank-monitor endpoints.
dev-container/settings.py
deploy/clowdapp.yaml
pulp_service/pulp_service/app/content_view_viewsets.py
pulp_service/pulp_service/app/viewsets.py
Adapt domain creation and migration endpoints to explicit authentication and authorization while preserving signal context safely.
  • Require IsAuthenticated for create and migrate API views.
  • Set domain-create context explicitly and clear ContextVars in a finally block.
  • Enforce core.change_domain for domain migration.
pulp_service/pulp_service/app/authorization.py
pulp_service/pulp_service/app/viewsets.py
Extend PulpServiceAccessPolicy to honor public-domain, subscription, readonly-group, admin-readonly, and domain-content access consistently during permission checks and queryset scoping.
  • Add configured domain-policy and admin-readonly read grants.
  • Preserve orphan-content visibility for users with domain-scoped view permission.
  • Apply public-domain bypasses to queryset scoping and restrict global Domain listings to authorized or public domains.
  • Avoid unscoped returns for global models to maintain tenant isolation.
pulp_service/pulp_service/app/access_policy.py
Restore and expand RBAC regression coverage, reconciling expected status codes and test setup with the new authorization behavior.
  • Restore tests for orphan content, public-domain reads, org-member access, missing internal org IDs, and historical role backfill.
  • Migrate domain creation fixtures to the self-service endpoint and validate dual-written RBAC roles.
  • Update repository-list assertions to expect empty 200 responses when queryset scoping hides unauthorized objects, while retaining 403 assertions for content-policy denials.
  • Cover admin-readonly, subscription, readonly-group, content-guard, domain isolation, and non-public-domain authentication behavior.
pulp_service/pulp_service/tests/functional/test_admin_readonly_group.py
pulp_service/pulp_service/tests/functional/test_authentication.py
pulp_service/pulp_service/tests/functional/test_content_guard_permission.py
pulp_service/pulp_service/tests/functional/test_content_view_after_upload.py
pulp_service/pulp_service/tests/functional/test_create_without_internal_org_id.py
pulp_service/pulp_service/tests/functional/test_domain_based_permissions.py
pulp_service/pulp_service/tests/functional/test_domain_dual_write.py
pulp_service/pulp_service/tests/functional/test_group_based_permissions.py
pulp_service/pulp_service/tests/functional/test_lightwell_content_listing_permission.py
pulp_service/pulp_service/tests/functional/test_lightwell_readonly_group_permission.py
pulp_service/pulp_service/tests/functional/test_org_member_historical_backfill.py
pulp_service/pulp_service/tests/functional/test_org_member_rbac_access.py
pulp_service/pulp_service/tests/functional/test_public_domain_read.py
Document the authorization switchover and compatibility fixes in the release notes.
  • Document the new default RBAC policy and explicit domain endpoint checks.
  • Document orphan-content access, public-domain reads, and organization-ID derivation/backfill behavior.
CHANGES/enable-rbac.feature

Possibly linked issues

  • #PULP-1649: The PR directly fixes domain-list leakage by scoping Domain querysets under PulpServiceAccessPolicy, though it also introduces broader RBAC changes.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

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.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="dev-container/settings.py" line_range="49" />
<code_context>

 REST_FRAMEWORK__DEFAULT_PERMISSION_CLASSES = (
-    "pulp_service.app.authorization.DomainBasedPermission",
+    "pulp_service.app.access_policy.PulpServiceAccessPolicy",
 )

</code_context>
<issue_to_address>
**issue (broader_impact):** Generic `DomainsApi` domain creation no longer runs `DomainBasedPermission.has_permission`, which previously populated `user_id_var` and `org_id_var` before the `post_create_domain` signal. When an RBAC user still has `core.add_domain` and creates a domain through that generic endpoint, the signal receives no creator context, skips the DomainOrg/RBAC role dual-write and default content-guard provisioning, and leaves the newly created domain without its expected access assignments.

**Triggers:** When a non-superuser with `core.add_domain` creates a domain through the generic DomainsApi rather than `/create-domain/`.

**Suggested fix:** Ensure the generic domain-create path also calls `set_domain_create_context` and cleans up the context, or explicitly deny generic non-admin domain creation and route all permitted creation through `CreateDomainView`.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 1 finding to address first, and this changes the default permission class for the entire REST API, including domain creation, object scoping, public-domain bypasses, and subscription-based grants; an incorrect decision could broadly expose tenant data or authorize unintended operations from the moment it ships. Reverting stops future requests under the new policy, but it cannot undo access or data exposure that already occurred.

Blocking findings: dev-container/settings.py:49


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread dev-container/settings.py
@CryptoRodeo

Copy link
Copy Markdown
Contributor Author

@dkliban RBAC enablement PR

@CryptoRodeo
CryptoRodeo marked this pull request as draft September 29, 2026 12:56
@CryptoRodeo
CryptoRodeo force-pushed the feat/enable-rbac branch 3 times, most recently from 1857749 to bdd9759 Compare September 29, 2026 13:57
@CryptoRodeo

Copy link
Copy Markdown
Contributor Author

/retest

@red-hat-konflux

Copy link
Copy Markdown
Contributor

All PipelineRuns for this commit have already succeeded. Use /retest <pipeline-name> to re-run a specific pipeline or /test to re-run all pipelines.

@CryptoRodeo

Copy link
Copy Markdown
Contributor Author

/retest pulp-bonfire-tekton

@CryptoRodeo
CryptoRodeo marked this pull request as ready for review September 29, 2026 14:42

@sourcery-ai sourcery-ai Bot left a comment

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.

Sorry @CryptoRodeo, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 6 days and 8 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@CryptoRodeo
CryptoRodeo force-pushed the feat/enable-rbac branch 2 times, most recently from e37696f to f629162 Compare September 29, 2026 16:50
@CryptoRodeo

Copy link
Copy Markdown
Contributor Author

@dkliban heads up, Patch 0018 patches pulp_container's RegistryPermission to subclass DomainBasedPermission, so the DomainBasedPermission class has to remain until we migrate that path. It's no longer the default anywhere and now survives only for the registry patch. Registry auth is unchanged from main, so there should be no regression.

@dkliban

dkliban commented Sep 29, 2026

Copy link
Copy Markdown
Member

@dkliban heads up, Patch 0018 patches pulp_container's RegistryPermission to subclass DomainBasedPermission, so the DomainBasedPermission class has to remain until we migrate that path. It's no longer the default anywhere and now survives only for the registry patch. Registry auth is unchanged from main, so there should be no regression.

We can remove patch 18. It was only needed so we could use DomainBasedPermission class for pulp_container.

This flips the default permission class from the old DomainBasedPermission to
PulpServiceAccessPolicy, so RBAC is actually the thing enforcing access now
instead of the org_id/DomainOrg checks. The settings change lives in both
dev-container/settings.py and deploy/clowdapp.yaml; the code side updates the
few viewsets that hard-coded the old class:

- PyPIYankMonitorViewSet drops DomainBasedPermission.
- CreateDomainView / MigrateDomainView move to [IsAuthenticated] plus explicit
  checks (set_domain_create_context for create, has_perm("core.change_domain")
  for migrate) so they don't lean on the old permission class implicitly.

The bulk of the diff is test reconciliation. A pile of RBAC-focused functional
tests had been merged to main but couldn't run for real until RBAC was on, so
they're restored here, plus the existing suite is updated where it asserted the
old permission model's behavior. The one thing worth calling out: under RBAC,
"denied" doesn't always mean 403.

- Repository listing lets any authenticated user in and scopes the queryset by
  role, so a caller with no role on the domain gets an empty 200 (no leak), not
  a 403. Those assertions changed to 200 + count == 0.
- Content listing is different: ACCESS_POLICIES["content/file/files"] gates the
  list action on has_domain_perms:core.view_content, so a caller without that
  permission is denied outright with a 403. Those assertions stay 403.
- Non-public domains now get a default identity content guard, so anonymous
  GETs against them are denied. The one auth test that assumed an unguarded
  domain now uses a public- domain (the only unguarded case).

The main thing I set out to protect: the orphaned-content fix still works under
RBAC. When you upload content that isn't in any repository yet, the domain owner
still has to be able to read it back. PulpServiceAccessPolicy.scope_queryset
handles this first (via _is_domain_content_read) before any of the other
read-grant branches, so a domain member holding core.view_content gets the
content queryset back unscoped. Verified end-to-end:
test_content_view_after_upload.py passes 3/3 in the dev container (owner reads
orphan content via both the generic and typed endpoints; an unrelated org is
denied with 403).

One tenant-isolation guard: scope_queryset's per-domain read grants
(DOMAIN_ACCESS_POLICIES subscription/readonly-group) only return a queryset
unscoped for models base.py already domain-scoped (those with a pulp_domain FK).
Global models (Domain, Group, User, Role) fall through instead -- Domain to its
dedicated block that shows only the policy domain + public-*. Without this a
Lightwell-ReadOnly member hitting GET /api/pulp/lightwell/api/v3/domains/ would
have listed every tenant's domain. admin-readonly stays a global read by design.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Bryan ramos <bramos@redhat.com>
@CryptoRodeo

Copy link
Copy Markdown
Contributor Author

@dkliban
dkliban merged commit fd77d57 into pulp:main Sep 30, 2026
6 checks passed
CryptoRodeo added a commit to CryptoRodeo/pulp-service that referenced this pull request Sep 30, 2026
Enabling RBAC (pulp#1535) made PulpServiceAccessPolicy the default, and
pulpcore's AccessPolicyFromSettings *replaces* rather than merges a
viewset's policy with the matching settings.ACCESS_POLICIES entry. That
silently 403'd three endpoints for non-superuser domain-admin service
accounts: content/file/files shared a list-only policy that dropped
pulp_file's create/upload/label statements; artifacts/ kept pulpcore's
admin-only default because it was never overridden; and orphans/cleanup,
a urlpattern-less ViewSet the settings lookup can't reach, fell through to
the admin-only module default.

Give content/file/files its own policy that reproduces pulp_file's
create/upload/set_label/unset_label statements verbatim while keeping the
orphan-content read goal (list/retrieve on core.view_content,
queryset_scoping=None). Add an artifacts policy opening read+create to
domain admins (destroy stays admin-only). Open orphans/cleanup by setting
DEFAULT_ACCESS_POLICY on the class at plugin startup from ready(), the only
path that reaches a viewset with no urlpattern. Adds structural unit guards
and behavioral functional tests for all three.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Bryan ramos <bramos@redhat.com>
CryptoRodeo added a commit to CryptoRodeo/pulp-service that referenced this pull request Sep 30, 2026
Enabling RBAC (pulp#1535) made PulpServiceAccessPolicy the default, and
pulpcore's AccessPolicyFromSettings *replaces* rather than merges a
viewset's policy with the matching settings.ACCESS_POLICIES entry. That
silently 403'd three endpoints for non-superuser domain-admin service
accounts: content/file/files shared a list-only policy that dropped
pulp_file's create/upload/label statements; artifacts/ kept pulpcore's
admin-only default because it was never overridden; and orphans/cleanup,
a urlpattern-less ViewSet the settings lookup can't reach, fell through to
the admin-only module default.

Give content/file/files its own policy that reproduces pulp_file's
create/upload/set_label/unset_label statements verbatim while keeping the
orphan-content read goal (list/retrieve on core.view_content,
queryset_scoping=None). Add an artifacts policy opening read+create to
domain admins (destroy stays admin-only). Open orphans/cleanup by setting
DEFAULT_ACCESS_POLICY on the class at plugin startup from ready(), the only
path that reaches a viewset with no urlpattern. Adds structural unit guards
and behavioral functional tests for all three.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Bryan ramos <bramos@redhat.com>
CryptoRodeo added a commit to CryptoRodeo/pulp-service that referenced this pull request Sep 30, 2026
Enabling RBAC (pulp#1535) made PulpServiceAccessPolicy the default, and
pulpcore's AccessPolicyFromSettings *replaces* rather than merges a
viewset's policy with the matching settings.ACCESS_POLICIES entry. That
silently 403'd three endpoints for non-superuser domain-admin service
accounts: content/file/files shared a list-only policy that dropped
pulp_file's create/upload/label statements; artifacts/ kept pulpcore's
admin-only default because it was never overridden; and orphans/cleanup,
a urlpattern-less ViewSet the settings lookup can't reach, fell through to
the admin-only module default.

Give content/file/files its own policy that reproduces pulp_file's
create/upload/set_label/unset_label statements verbatim while keeping the
orphan-content read goal (list/retrieve on core.view_content,
queryset_scoping=None). Add an artifacts policy opening read+create to
domain admins (destroy stays admin-only). Open orphans/cleanup by setting
DEFAULT_ACCESS_POLICY on the class at plugin startup from ready(), the only
path that reaches a viewset with no urlpattern. Adds structural unit guards
and behavioral functional tests for all three.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Bryan ramos <bramos@redhat.com>
dkliban pushed a commit that referenced this pull request Sep 30, 2026
Enabling RBAC (#1535) made PulpServiceAccessPolicy the default, and
pulpcore's AccessPolicyFromSettings *replaces* rather than merges a
viewset's policy with the matching settings.ACCESS_POLICIES entry. That
silently 403'd three endpoints for non-superuser domain-admin service
accounts: content/file/files shared a list-only policy that dropped
pulp_file's create/upload/label statements; artifacts/ kept pulpcore's
admin-only default because it was never overridden; and orphans/cleanup,
a urlpattern-less ViewSet the settings lookup can't reach, fell through to
the admin-only module default.

Give content/file/files its own policy that reproduces pulp_file's
create/upload/set_label/unset_label statements verbatim while keeping the
orphan-content read goal (list/retrieve on core.view_content,
queryset_scoping=None). Add an artifacts policy opening read+create to
domain admins (destroy stays admin-only). Open orphans/cleanup by setting
DEFAULT_ACCESS_POLICY on the class at plugin startup from ready(), the only
path that reaches a viewset with no urlpattern. Adds structural unit guards
and behavioral functional tests for all three.

Signed-off-by: Bryan ramos <bramos@redhat.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants