Skip to content

Conversation

@git-hyagi
Copy link
Collaborator

@git-hyagi git-hyagi commented Jan 7, 2026

ref: https://issues.redhat.com/browse/PULP-1065

Summary by Sourcery

Allow unauthenticated read-only access to the PyPI SimpleView for Python distributions in explicitly marked public domains while strengthening authentication configuration and coverage.

New Features:

  • Introduce a PublicDomainPermission that grants GET-only access to domains whose name is prefixed with public- for Python SimpleView traffic.
  • Enable entitlement-certificate-based authentication as an additional default REST framework authentication class.

Bug Fixes:

  • Prevent x-rh-identity authentication headers from leaking between functional tests by cleaning bindings headers between tests.

CI:

  • Add a Tekton task to run pulp_service functional tests against ephemeral environments in pull requests and integrate it into the deployment-and-test pipeline.

Tests:

  • Add functional authentication tests covering x-rh-identity variants and public vs private domain access behavior for the PyPI simple endpoint.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jan 7, 2026

Reviewer's Guide

Adds a new PublicDomainPermission for unauthenticated GET access to public- prefixed domains, wires additional authentication into default REST framework settings, extends Tekton CI with pulp-service functional tests, and introduces functional tests plus fixtures to validate x-rh-identity handling and public vs private PyPI Simple endpoint behavior.

Sequence diagram for public domain GET permission evaluation

sequenceDiagram
    actor User
    participant DjangoApp
    participant PublicDomainPermission
    participant DomainModel

    User->>DjangoApp: GET /pypi/simple/
    DjangoApp->>PublicDomainPermission: has_permission(request, view)
    PublicDomainPermission->>PublicDomainPermission: check request.method == GET
    alt method_is_not_GET
        PublicDomainPermission-->>DjangoApp: return False
        DjangoApp-->>User: 403 Forbidden
    else method_is_GET
        PublicDomainPermission->>PublicDomainPermission: domain_pk = get_domain_pk()
        PublicDomainPermission->>DomainModel: Domain.objects.get(pk=domain_pk)
        alt domain_name_starts_with_public_prefix
            DomainModel-->>PublicDomainPermission: domain(name=public-example)
            PublicDomainPermission-->>DjangoApp: return True
            DjangoApp-->>User: 200 OK
        else not_public_or_error
            DomainModel-->>PublicDomainPermission: domain(name=private-example) or exception
            PublicDomainPermission-->>DjangoApp: return False
            DjangoApp-->>User: 403 Forbidden
        end
    end
Loading

Class diagram for new PublicDomainPermission and related types

classDiagram
    class BasePermission {
    }

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

    class Domain {
        +pk int
        +name string
        +objects DomainManager
    }

    class DomainManager {
        +get(pk int) Domain
    }

    class Utilities {
        +get_domain_pk() int
    }

    PublicDomainPermission --|> BasePermission
    Domain o-- DomainManager
    PublicDomainPermission ..> Domain : uses
    PublicDomainPermission ..> Utilities : calls
Loading

File-Level Changes

Change Details Files
Add Tekton pipeline task to run pulp_service functional tests against ephemeral environments and integrate it into the deployment/test flow.
  • Introduce pulp-service-functional-tests task that logs into the ephemeral OpenShift cluster, discovers the namespace, and retrieves the admin password and pod info.
  • Reuse oc_wrapper-based cmd_prefix helpers to run commands inside the pulp-api container.
  • Set up pulp-smash configuration and test requirements inside the running pod and install needed Python dependencies and tools.
  • Execute pulp_service functional tests (non-parallel) via pytest with appropriate environment variables, and ensure the final test-summary task waits on this new task.
.tekton/pulp-deploy-and-test.yaml
Introduce cleanup fixture to clear x-rh-identity headers between tests to avoid cross-test contamination.
  • Add cleanup_auth_headers pytest fixture that strips x-rh-identity from pulpcore DomainsApi client headers.
  • Extend the fixture to optionally clear x-rh-identity headers from file_bindings and python_bindings API clients when those fixtures are in use.
pulp_service/pulp_service/tests/functional/conftest.py
Add PublicDomainPermission that allows unauthenticated GET access for public- prefixed Domains and wire in additional REST framework authentication.
  • Implement PublicDomainPermission permission class that checks current domain via get_domain_pk/Domain and grants permission only for GET requests on domains whose name starts with public-, otherwise denies access.
  • Update default REST framework authentication classes in the ClowdApp config to include RHEntitlementCertAuthentication while keeping existing authenticators intact.
  • Confirm that default permission classes remain DomainBasedPermission, to be combined with the new permission where appropriate (e.g., on PyPI Simple views via separate configuration/patching).
pulp_service/pulp_service/app/authorization.py
deploy/clowdapp.yaml
images/assets/patches/0023-Add-auth-override-pulp-python.patch
Add functional tests covering x-rh-identity-based authentication semantics and public vs private PyPI Simple endpoint access patterns.
  • Add tests verifying that x-rh-identity headers containing username and/or org_id values permit domain creation and Python repository operations via pulpcore and python bindings.
  • Add tests ensuring that the PyPI Simple endpoint for domains whose names start with public- is accessible without authentication for GET only, while non-GET methods return 401/403.
  • Add tests confirming that non-public domains (e.g., private- prefixed) deny unauthenticated access to the PyPI Simple endpoint (expecting 401/403) and clean up any x-rh-identity headers set on bindings to avoid leakage.
pulp_service/pulp_service/tests/functional/test_authentication.py

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

@git-hyagi git-hyagi force-pushed the pypi-public-domain-access branch 7 times, most recently from da71b22 to a0e7e35 Compare January 8, 2026 20:26
@git-hyagi git-hyagi force-pushed the pypi-public-domain-access branch from a0e7e35 to 0b6f3d7 Compare January 9, 2026 11:29
@git-hyagi git-hyagi marked this pull request as ready for review January 9, 2026 12:08
@git-hyagi git-hyagi merged commit 825ed23 into pulp:main Jan 9, 2026
0 of 4 checks passed
@git-hyagi git-hyagi deleted the pypi-public-domain-access branch January 9, 2026 12:09
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 found 2 issues, and left some high level feedback:

  • In PublicDomainPermission.has_permission, consider narrowing the broad except Exception to specific errors like Domain.DoesNotExist (and potentially ValueError/TypeError from get_domain_pk()), so unexpected failures don’t get silently turned into authorization denials and are easier to debug.
  • The PublicDomainPermission currently only allows GET requests; if you want to support typical read-only semantics, you may want to explicitly handle HEAD (and possibly OPTIONS) as allowed methods for public domains rather than denying them.
  • The cleanup_auth_headers fixture and the explicit header cleanup at the end of test_pypi_simple_endpoint_private_domain_without_auth_blocked partially duplicate behavior; consider consolidating this cleanup logic into the fixture (e.g., using yield and always running after the test) to avoid header-leakage bugs and make it easier to maintain.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `PublicDomainPermission.has_permission`, consider narrowing the broad `except Exception` to specific errors like `Domain.DoesNotExist` (and potentially `ValueError`/`TypeError` from `get_domain_pk()`), so unexpected failures don’t get silently turned into authorization denials and are easier to debug.
- The `PublicDomainPermission` currently only allows `GET` requests; if you want to support typical read-only semantics, you may want to explicitly handle `HEAD` (and possibly `OPTIONS`) as allowed methods for public domains rather than denying them.
- The `cleanup_auth_headers` fixture and the explicit header cleanup at the end of `test_pypi_simple_endpoint_private_domain_without_auth_blocked` partially duplicate behavior; consider consolidating this cleanup logic into the fixture (e.g., using `yield` and always running after the test) to avoid header-leakage bugs and make it easier to maintain.

## Individual Comments

### Comment 1
<location> `pulp_service/pulp_service/app/authorization.py:133-138` </location>
<code_context>
+        if request.method != "GET":
+            return False
+
+        try:
+            domain_pk = get_domain_pk()
+            domain = Domain.objects.get(pk=domain_pk)
+            if domain.name.startswith("public-"):
+                return True
+        except Exception:
+            # If we can't get the domain, deny access
+            return False
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Avoid catching bare `Exception` and narrow to expected error types.

Catching bare `Exception` here can mask real bugs (e.g. programming or DB errors) and make them harder to detect. Prefer catching only the expected cases, e.g. `Domain.DoesNotExist` and the specific exceptions `get_domain_pk()` can raise (such as `ValueError`/`TypeError`), and let anything else surface instead of being converted into a generic 403/denial.
</issue_to_address>

### Comment 2
<location> `pulp_service/pulp_service/tests/functional/test_authentication.py:345-354` </location>
<code_context>
+        api_host = python_bindings.DistributionsPypiApi.api_client.configuration.host
+        simple_url = f"{api_host}/api/pypi/{domain_name}/{base_path}/simple/"
+
+        # This should be blocked for non-public domains
+        # 401 = Unauthorized, 403 = Forbidden
+        response = requests.get(simple_url)
+        assert response.status_code in [401, 403]
+
+        # some bindings has scope=session, so we need to remove the headers to avoid
+        # affecting the other tests
+        pulpcore_bindings.DomainsApi.api_client.default_headers.pop("x-rh-identity", None)
+        python_bindings.RepositoriesPythonApi.api_client.default_headers.pop("x-rh-identity", None)
+        python_bindings.DistributionsPypiApi.api_client.default_headers.pop("x-rh-identity", None)
</code_context>

<issue_to_address>
**suggestion (testing):** Rely on a centralized header-cleanup mechanism instead of manually popping headers in this test.

Here you manually clear `x-rh-identity` from three bindings. Since we already have a `cleanup_auth_headers` fixture for preventing auth header leakage between tests, it would be better to rely on a shared mechanism instead of ad-hoc `pop` calls.

Consider either extending `cleanup_auth_headers` (e.g. via `yield` or `request.addfinalizer`) so it always runs after tests and covers all relevant bindings, then drop the manual cleanup here, or extracting this logic into a reusable helper/fixture used by all tests that modify `default_headers`. This will improve test isolation and reduce the risk of flakiness from shared client state.

Suggested implementation:

```python
        # This should be blocked for non-public domains
        # 401 = Unauthorized, 403 = Forbidden
        response = requests.get(simple_url)
        assert response.status_code in [401, 403]
        # Cleanup of auth-related headers is handled by the shared cleanup_auth_headers fixture.

```

1. Ensure this test function uses the `cleanup_auth_headers` fixture by adding it to the test function's parameter list, for example:
   - Change `def test_xxx(...):` to `def test_xxx(..., cleanup_auth_headers):`.
2. If `cleanup_auth_headers` currently only targets a subset of bindings, extend it so that it also clears `x-rh-identity` from:
   - `pulpcore_bindings.DomainsApi.api_client.default_headers`
   - `python_bindings.RepositoriesPythonApi.api_client.default_headers`
   - `python_bindings.DistributionsPypiApi.api_client.default_headers`
3. If the fixture is not in this file, confirm it is imported/available via `conftest.py` so that adding it as a parameter correctly triggers the centralized cleanup.
</issue_to_address>

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.

Comment on lines +133 to +138
try:
domain_pk = get_domain_pk()
domain = Domain.objects.get(pk=domain_pk)
if domain.name.startswith("public-"):
return True
except Exception:
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): Avoid catching bare Exception and narrow to expected error types.

Catching bare Exception here can mask real bugs (e.g. programming or DB errors) and make them harder to detect. Prefer catching only the expected cases, e.g. Domain.DoesNotExist and the specific exceptions get_domain_pk() can raise (such as ValueError/TypeError), and let anything else surface instead of being converted into a generic 403/denial.

Comment on lines +345 to +354
# This should be blocked for non-public domains
# 401 = Unauthorized, 403 = Forbidden
response = requests.get(simple_url)
assert response.status_code in [401, 403]

# some bindings has scope=session, so we need to remove the headers to avoid
# affecting the other tests
pulpcore_bindings.DomainsApi.api_client.default_headers.pop("x-rh-identity", None)
python_bindings.RepositoriesPythonApi.api_client.default_headers.pop("x-rh-identity", None)
python_bindings.DistributionsPypiApi.api_client.default_headers.pop("x-rh-identity", None)
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (testing): Rely on a centralized header-cleanup mechanism instead of manually popping headers in this test.

Here you manually clear x-rh-identity from three bindings. Since we already have a cleanup_auth_headers fixture for preventing auth header leakage between tests, it would be better to rely on a shared mechanism instead of ad-hoc pop calls.

Consider either extending cleanup_auth_headers (e.g. via yield or request.addfinalizer) so it always runs after tests and covers all relevant bindings, then drop the manual cleanup here, or extracting this logic into a reusable helper/fixture used by all tests that modify default_headers. This will improve test isolation and reduce the risk of flakiness from shared client state.

Suggested implementation:

        # This should be blocked for non-public domains
        # 401 = Unauthorized, 403 = Forbidden
        response = requests.get(simple_url)
        assert response.status_code in [401, 403]
        # Cleanup of auth-related headers is handled by the shared cleanup_auth_headers fixture.
  1. Ensure this test function uses the cleanup_auth_headers fixture by adding it to the test function's parameter list, for example:
    • Change def test_xxx(...): to def test_xxx(..., cleanup_auth_headers):.
  2. If cleanup_auth_headers currently only targets a subset of bindings, extend it so that it also clears x-rh-identity from:
    • pulpcore_bindings.DomainsApi.api_client.default_headers
    • python_bindings.RepositoriesPythonApi.api_client.default_headers
    • python_bindings.DistributionsPypiApi.api_client.default_headers
  3. If the fixture is not in this file, confirm it is imported/available via conftest.py so that adding it as a parameter correctly triggers the centralized cleanup.

Comment on lines +53 to +55
- authentication_classes = []
- permission_classes = []
+ permission_classes = [PublicDomainPermission|DomainBasedPermission]
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

  • we still need to authenticate because the permission_classes will use the request.user to validate permissions (so we cannot set authentication_classes=[])
  • the permission classes work in a composed way, so in this patch it will check if PublicDomainPermission OR DomainBasedPermission passed
  • if no authentication class succeeds, request.user defaults to an AnonymousUser instance, request.auth is set to None, and drf will not return 401 to the client yet, it will now check the permission_classes (for public domains, the authentication will fail and drf will check the PublicDomainPermission)

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