Skip to content

PULP-1179: Restrict unauthenticated PyPI access to public- domains (using Serena)#926

Open
decko wants to merge 2 commits intopulp:mainfrom
decko:pulp-1179-public-domain-read-permission
Open

PULP-1179: Restrict unauthenticated PyPI access to public- domains (using Serena)#926
decko wants to merge 2 commits intopulp:mainfrom
decko:pulp-1179-public-domain-read-permission

Conversation

@decko
Copy link
Member

@decko decko commented Feb 6, 2026

Summary

Implements PULP-1179 to restrict unauthenticated GET/HEAD/OPTIONS requests to only domains whose names start with the public- prefix (case-sensitive).

Changes

  • Added PublicDomainReadPermission class in pulp_service/app/authorization.py

    • Only allows safe methods (GET/HEAD/OPTIONS)
    • Checks if domain name starts with public- (case-sensitive)
    • Handles all edge cases with appropriate logging
    • Fails closed (denies access) on any error
  • Updated patch 0038 to use PublicDomainReadPermission instead of AllowUnauthPull

    • PyPI endpoints now restrict unauthenticated access to public domains only
    • Maintains authenticated access for all domains via DomainBasedPermission
  • Added comprehensive tests in test_authentication.py:

    • Test public domains allow unauthenticated read
    • Test non-public domains block unauthenticated read
    • Test public domains block unsafe methods
    • Test case sensitivity (only lowercase public- works)

Backward Compatibility

This is an additive change. AllowUnauthPull remains unchanged and functional. Only PyPI endpoints are affected by the new permission class.

Test plan

  • Run new authentication tests for public domain permissions
  • Verify existing tests still pass (no regressions)
  • Manual verification with public and non-public domains
  • Check logs for proper error handling

🤖 Generated with Claude Code

Summary by Sourcery

Restrict unauthenticated PyPI read access to explicitly public domains and tighten domain-based authorization behavior.

New Features:

  • Introduce a PublicDomainReadPermission to allow unauthenticated safe-method access only for domains whose names start with the public- prefix.

Enhancements:

  • Apply the new public-domain read permission to PyPI endpoints so only designated public domains are anonymously readable while preserving authenticated access for all domains.

Build:

  • Apply an additional application patch during image build to extend the Python content endpoint with a contains filter.

Tests:

  • Add functional authentication tests covering unauthenticated access to public and non-public PyPI domains, unsafe HTTP methods, and case sensitivity of the public- prefix.

decko and others added 2 commits February 6, 2026 11:31
…public- domains.

This implements PULP-1179 by adding a new permission class that only allows
unauthenticated GET/HEAD/OPTIONS requests to domains whose names start with
the 'public-' prefix (case-sensitive). PyPI endpoints now use this permission
instead of AllowUnauthPull to restrict public read access.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Feb 6, 2026

Reviewer's Guide

Implements a new PublicDomainReadPermission to allow unauthenticated safe-method access only for PyPI distributions in domains whose names start with 'public-', wires this into the readonly PyPI endpoints via patch 0038, adds functional tests covering the permission behavior and case sensitivity, and updates the Docker image to apply an additional Python content endpoint patch (0042).

Sequence diagram for unauthenticated access to PyPI endpoint with PublicDomainReadPermission

sequenceDiagram
    actor Client
    participant PyPIEndpointView
    participant PublicDomainReadPermission
    participant get_domain_pk
    participant DomainModel as Domain

    Client->>PyPIEndpointView: HTTP request (method, path, headers)
    PyPIEndpointView->>PublicDomainReadPermission: has_permission(request, view)
    PublicDomainReadPermission->>PublicDomainReadPermission: check request.method in SAFE_METHODS
    alt method_not_safe
        PublicDomainReadPermission-->>PyPIEndpointView: False
        PyPIEndpointView-->>Client: 403 Forbidden
    else method_safe
        PublicDomainReadPermission->>get_domain_pk: __call__()
        alt domain_context_missing
            get_domain_pk-->>PublicDomainReadPermission: raises LookupError
            PublicDomainReadPermission-->>PyPIEndpointView: False
            PyPIEndpointView-->>Client: 403 Forbidden
        else domain_context_available
            get_domain_pk-->>PublicDomainReadPermission: domain_pk
            PublicDomainReadPermission->>DomainModel: objects.get(pk=domain_pk)
            alt domain_not_found
                DomainModel-->>PublicDomainReadPermission: raises DoesNotExist
                PublicDomainReadPermission-->>PyPIEndpointView: False
                PyPIEndpointView-->>Client: 403 Forbidden
            else domain_found
                DomainModel-->>PublicDomainReadPermission: domain(name)
                alt name_starts_with_public_prefix
                    PublicDomainReadPermission-->>PyPIEndpointView: True
                    PyPIEndpointView-->>Client: 200 OK (unauthenticated)
                else name_without_public_prefix
                    PublicDomainReadPermission-->>PyPIEndpointView: False
                    PyPIEndpointView-->>Client: 403 Forbidden
                end
            end
        end
    end
Loading

Class diagram for PublicDomainReadPermission and related authorization classes

classDiagram
    class BasePermission {
        <<abstract>>
        +has_permission(request, view) bool
    }

    class AllowUnauthPull {
        +has_permission(request, view) bool
    }

    class PublicDomainReadPermission {
        +has_permission(request, view) bool
    }

    class Domain {
        +pk int
        +name str
        +objects
    }

    class DomainManager {
        +get(pk int) Domain
    }

    class Logger {
        +debug(message str)
        +warning(message str)
        +error(message str)
    }

    class SAFE_METHODS {
        +methods list
    }

    class get_domain_pk {
        +__call__() int
    }

    BasePermission <|-- AllowUnauthPull
    BasePermission <|-- PublicDomainReadPermission

    Domain o-- DomainManager : objects

    PublicDomainReadPermission ..> Domain : fetches
    PublicDomainReadPermission ..> DomainManager : uses
    PublicDomainReadPermission ..> Logger : logs
    PublicDomainReadPermission ..> SAFE_METHODS : checks
    PublicDomainReadPermission ..> get_domain_pk : obtains_domain_context
Loading

File-Level Changes

Change Details Files
Introduce PublicDomainReadPermission to gate unauthenticated access by domain name prefix.
  • Add PublicDomainReadPermission BasePermission subclass that only allows SAFE_METHODS (GET/HEAD/OPTIONS).
  • Resolve domain context via get_domain_pk() with defensive error handling and logging, failing closed on any exception or missing domain.
  • Look up the Domain by primary key and allow access only when domain.name starts with the literal 'public-' (case-sensitive), otherwise deny.
pulp_service/pulp_service/app/authorization.py
Update PyPI readonly endpoint behavior to use the new permission and constrain unauthenticated reads to public- domains.
  • Modify readonly PyPI endpoint patch (0038) to use PublicDomainReadPermission instead of AllowUnauthPull so only public- domains remain world-readable.
  • Keep DomainBasedPermission for authenticated access so behavior for authenticated clients and other endpoints remains unchanged.
images/assets/patches/0038-readonly-pypi-endpoints.patch
Add functional tests verifying unauthenticated access rules for public and non-public domains, including method and case restrictions.
  • Add test that public- domains allow unauthenticated GET/HEAD/OPTIONS requests against the PyPI simple API.
  • Add test that non-public domains (e.g. private-...) reject unauthenticated GET/HEAD requests with 401/403.
  • Add test that public- domains still reject unauthenticated unsafe methods (POST/PUT/DELETE/PATCH), expecting 401/403/405.
  • Add test that domains with different casing (e.g. Public-) do not qualify as public, confirming case-sensitive prefix checking.
pulp_service/pulp_service/tests/functional/test_authentication.py
Adjust Docker image to apply an additional Python-related patch at build time.
  • Copy new 0042-Add-contains-filter-python-content-endpoint.patch into the image build context.
  • Apply the 0042 patch to the installed Pulp Python site-packages during image build.
Dockerfile
images/assets/patches/0042-Add-contains-filter-python-content-endpoint.patch

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

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • The new PublicDomainReadPermission catches broad Exception in multiple places and logs with warning/error; consider using more specific exception types or logger.exception where appropriate so you preserve tracebacks and make debugging permission failures easier.
  • The four new authentication tests are very similar in their setup (user, domain, repo, distribution); consider extracting a shared helper/fixture or parametrizing the tests to reduce duplication and make future changes to the setup less error-prone.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `PublicDomainReadPermission` catches broad `Exception` in multiple places and logs with `warning`/`error`; consider using more specific exception types or `logger.exception` where appropriate so you preserve tracebacks and make debugging permission failures easier.
- The four new authentication tests are very similar in their setup (user, domain, repo, distribution); consider extracting a shared helper/fixture or parametrizing the tests to reduce duplication and make future changes to the setup less error-prone.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@decko
Copy link
Member Author

decko commented Feb 6, 2026

❯ /cost
  ⎿  Total cost:            $3.14
     Total duration (API):  18m 7s
     Total duration (wall): 20m 34s
     Total code changes:    581 lines added, 7 lines removed
     Usage by model:
        claude-sonnet-4-5:  6.9k input, 31.0k output, 1.6m cache read, 118.5k cache write ($2.34)
         claude-haiku-4-5:  64.3k input, 24.4k output, 3.3m cache read, 224.8k cache write ($0.80)

@decko decko changed the title PULP-1179: Restrict unauthenticated PyPI access to public- domains PULP-1179: Restrict unauthenticated PyPI access to public- domains (using Serena) Feb 6, 2026
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.

1 participant