|
| 1 | +""" |
| 2 | +Functional tests for the lightwell-network feature check enforced by DomainBasedPermission, |
| 3 | +scoped specifically to the "lightwell" domain's PyPI views (simple API, package metadata, |
| 4 | +etc.). Other domains' PyPI views, and all non-PyPI endpoints (even within the lightwell |
| 5 | +domain), keep the pre-existing permission model unaffected by this feature check. |
| 6 | +
|
| 7 | +These follow the pattern used in test_feature_service.py: they exercise the real Features |
| 8 | +Service (no mocking) using known staging accounts. Org LIGHTWELL_ENTITLED_ORG_ID has the |
| 9 | +lightwell-network feature; org LIGHTWELL_NOT_ENTITLED_ORG_ID does not. |
| 10 | +
|
| 11 | +NOTE: the feature check is keyed off the literal domain name "lightwell" (see |
| 12 | +pulp_service.app.authorization.LIGHTWELL_DOMAIN_NAME), so the domain created here can't use |
| 13 | +a random per-test suffix like most other functional tests in this suite. These tests assume |
| 14 | +they run against an ephemeral Pulp instance where no "lightwell" domain already exists. |
| 15 | +""" |
| 16 | + |
| 17 | +import json |
| 18 | +from base64 import b64encode |
| 19 | +from urllib.parse import urljoin |
| 20 | +from uuid import uuid4 |
| 21 | + |
| 22 | +import pytest |
| 23 | +import requests |
| 24 | + |
| 25 | +from pulp_service.tests.functional.constants import ( |
| 26 | + LIGHTWELL_ENTITLED_ORG_ID, |
| 27 | + LIGHTWELL_NOT_ENTITLED_ORG_ID, |
| 28 | +) |
| 29 | + |
| 30 | +LIGHTWELL_DOMAIN_NAME = "lightwell" |
| 31 | + |
| 32 | +# An org with no DomainOrg association with the test domains and no lightwell-network |
| 33 | +# feature entitlement; only used to own the test domain/repo/distribution. |
| 34 | +DOMAIN_OWNER_ORG_ID = "555555555" |
| 35 | + |
| 36 | + |
| 37 | +def _identity_header(org_id, username): |
| 38 | + identity = { |
| 39 | + "identity": { |
| 40 | + "org_id": org_id, |
| 41 | + "internal": {"org_id": org_id}, |
| 42 | + "user": {"username": username}, |
| 43 | + } |
| 44 | + } |
| 45 | + return b64encode(json.dumps(identity).encode()).decode() |
| 46 | + |
| 47 | + |
| 48 | +@pytest.fixture |
| 49 | +def configure_pypi_distribution( |
| 50 | + anonymous_user, |
| 51 | + gen_object_with_cleanup, |
| 52 | + pulpcore_bindings, |
| 53 | + python_bindings, |
| 54 | + bindings_cfg, |
| 55 | +): |
| 56 | + """ |
| 57 | + Creates a domain owned by DOMAIN_OWNER_ORG_ID, with a Python repository and a PyPI |
| 58 | + distribution. |
| 59 | +
|
| 60 | + Returns a (domain_name, pypi_simple_url, repos_url, owner_header) tuple. |
| 61 | + """ |
| 62 | + owner_header = _identity_header(DOMAIN_OWNER_ORG_ID, "lightwell-test-owner") |
| 63 | + |
| 64 | + def _configure(domain_name): |
| 65 | + with anonymous_user: |
| 66 | + pulpcore_bindings.DomainsApi.api_client.default_headers["x-rh-identity"] = owner_header |
| 67 | + |
| 68 | + gen_object_with_cleanup( |
| 69 | + pulpcore_bindings.DomainsApi, |
| 70 | + { |
| 71 | + "name": domain_name, |
| 72 | + "storage_class": "pulpcore.app.models.storage.FileSystem", |
| 73 | + "storage_settings": {"MEDIA_ROOT": "/var/lib/pulp/media/"}, |
| 74 | + }, |
| 75 | + ) |
| 76 | + |
| 77 | + python_bindings.RepositoriesPythonApi.api_client.default_headers["x-rh-identity"] = owner_header |
| 78 | + repo = gen_object_with_cleanup( |
| 79 | + python_bindings.RepositoriesPythonApi, {"name": str(uuid4())}, pulp_domain=domain_name |
| 80 | + ) |
| 81 | + |
| 82 | + python_bindings.DistributionsPypiApi.api_client.default_headers["x-rh-identity"] = owner_header |
| 83 | + base_path = str(uuid4()) |
| 84 | + gen_object_with_cleanup( |
| 85 | + python_bindings.DistributionsPypiApi, |
| 86 | + {"name": str(uuid4()), "base_path": base_path, "repository": repo.pulp_href}, |
| 87 | + pulp_domain=domain_name, |
| 88 | + ) |
| 89 | + |
| 90 | + pypi_url = urljoin(bindings_cfg.host, f"/api/pypi/{domain_name}/{base_path}/simple/") |
| 91 | + repos_url = urljoin(bindings_cfg.host, f"/api/pulp/{domain_name}/api/v3/repositories/python/python/") |
| 92 | + return domain_name, pypi_url, repos_url, owner_header |
| 93 | + |
| 94 | + yield _configure |
| 95 | + |
| 96 | + pulpcore_bindings.DomainsApi.api_client.default_headers.pop("x-rh-identity", None) |
| 97 | + python_bindings.RepositoriesPythonApi.api_client.default_headers.pop("x-rh-identity", None) |
| 98 | + python_bindings.DistributionsPypiApi.api_client.default_headers.pop("x-rh-identity", None) |
| 99 | + |
| 100 | + |
| 101 | +@pytest.fixture |
| 102 | +def configure_lightwell_pypi_distribution(configure_pypi_distribution): |
| 103 | + """Same as configure_pypi_distribution, but uses the literal "lightwell" domain name that |
| 104 | + DomainBasedPermission gates behind the lightwell-network feature.""" |
| 105 | + |
| 106 | + def _configure(): |
| 107 | + return configure_pypi_distribution(LIGHTWELL_DOMAIN_NAME) |
| 108 | + |
| 109 | + return _configure |
| 110 | + |
| 111 | + |
| 112 | +def test_org_without_feature_denied_on_lightwell_pypi_simple_api(configure_lightwell_pypi_distribution): |
| 113 | + """A user whose org doesn't have the lightwell-network feature and has no DomainOrg |
| 114 | + association gets 403 on the lightwell domain's PyPI simple API.""" |
| 115 | + _, pypi_url, _, _ = configure_lightwell_pypi_distribution() |
| 116 | + headers = {"x-rh-identity": _identity_header(LIGHTWELL_NOT_ENTITLED_ORG_ID, "not-entitled-user")} |
| 117 | + |
| 118 | + response = requests.get(pypi_url, headers=headers, timeout=30) |
| 119 | + |
| 120 | + assert response.status_code == 403 |
| 121 | + |
| 122 | + |
| 123 | +def test_org_with_feature_allowed_on_lightwell_pypi_simple_api(configure_lightwell_pypi_distribution): |
| 124 | + """A user whose org has the lightwell-network feature can read the lightwell domain's |
| 125 | + PyPI simple API, even without a DomainOrg association.""" |
| 126 | + _, pypi_url, _, _ = configure_lightwell_pypi_distribution() |
| 127 | + headers = {"x-rh-identity": _identity_header(LIGHTWELL_ENTITLED_ORG_ID, "entitled-user")} |
| 128 | + |
| 129 | + response = requests.get(pypi_url, headers=headers, timeout=30) |
| 130 | + |
| 131 | + assert response.status_code == 200 |
| 132 | + |
| 133 | + |
| 134 | +def test_domain_org_association_bypasses_feature_check(configure_lightwell_pypi_distribution): |
| 135 | + """The domain owner (has a DomainOrg association) can read the lightwell domain's PyPI |
| 136 | + simple API regardless of the lightwell-network feature.""" |
| 137 | + _, pypi_url, _, owner_header = configure_lightwell_pypi_distribution() |
| 138 | + headers = {"x-rh-identity": owner_header} |
| 139 | + |
| 140 | + response = requests.get(pypi_url, headers=headers, timeout=30) |
| 141 | + |
| 142 | + assert response.status_code == 200 |
| 143 | + |
| 144 | + |
| 145 | +def test_unauthenticated_denied_on_lightwell_pypi_simple_api(configure_lightwell_pypi_distribution): |
| 146 | + """Without any identity at all (no org_id to check a feature for), the lightwell domain's |
| 147 | + PyPI simple API must not be readable.""" |
| 148 | + _, pypi_url, _, _ = configure_lightwell_pypi_distribution() |
| 149 | + |
| 150 | + response = requests.get(pypi_url, timeout=30) |
| 151 | + |
| 152 | + assert response.status_code in (401, 403) |
| 153 | + |
| 154 | + |
| 155 | +def test_write_operations_unaffected_by_feature_check(configure_lightwell_pypi_distribution): |
| 156 | + """The lightwell-network feature only grants read access -- an entitled org with no |
| 157 | + DomainOrg association must still be denied write access to the lightwell domain's PyPI |
| 158 | + views.""" |
| 159 | + _, pypi_url, _, _ = configure_lightwell_pypi_distribution() |
| 160 | + headers = {"x-rh-identity": _identity_header(LIGHTWELL_ENTITLED_ORG_ID, "entitled-write-user")} |
| 161 | + |
| 162 | + response = requests.post(pypi_url, headers=headers, data={}, timeout=30) |
| 163 | + |
| 164 | + assert response.status_code in (401, 403) |
| 165 | + |
| 166 | + |
| 167 | +def test_non_pypi_endpoints_unaffected_by_feature_check(configure_lightwell_pypi_distribution): |
| 168 | + """Non-PyPI endpoints (here, the Pulp REST API's repository listing) must keep using the |
| 169 | + existing DomainOrg-based permission model, even within the lightwell domain: the |
| 170 | + lightwell-network feature grants no access to them.""" |
| 171 | + _, _, repos_url, _ = configure_lightwell_pypi_distribution() |
| 172 | + headers = {"x-rh-identity": _identity_header(LIGHTWELL_ENTITLED_ORG_ID, "entitled-rest-user")} |
| 173 | + |
| 174 | + response = requests.get(repos_url, headers=headers, timeout=30) |
| 175 | + |
| 176 | + assert response.status_code == 403 |
| 177 | + |
| 178 | + |
| 179 | +def test_other_domains_pypi_simple_api_unaffected_by_feature_check(configure_pypi_distribution): |
| 180 | + """Domains other than "lightwell" must keep the pre-existing behavior: any SAFE_METHOD |
| 181 | + request -- even unauthenticated -- can read the PyPI simple API, regardless of the |
| 182 | + lightwell-network feature.""" |
| 183 | + domain_name = f"not-lightwell-{uuid4()}" |
| 184 | + _, pypi_url, _, _ = configure_pypi_distribution(domain_name) |
| 185 | + |
| 186 | + response = requests.get(pypi_url, timeout=30) |
| 187 | + |
| 188 | + assert response.status_code == 200 |
| 189 | + |
| 190 | + |
| 191 | +def test_public_domain_allows_unauthenticated_pypi_access(configure_pypi_distribution): |
| 192 | + """A public- domain's PyPI simple API stays open to unauthenticated SAFE_METHOD |
| 193 | + requests -- unaffected by the lightwell-network feature check (which never applies to |
| 194 | + public- domains, or to non-"lightwell" domains in general).""" |
| 195 | + domain_name = f"public-{uuid4()}" |
| 196 | + _, pypi_url, _, _ = configure_pypi_distribution(domain_name) |
| 197 | + |
| 198 | + response = requests.get(pypi_url, timeout=30) |
| 199 | + |
| 200 | + assert response.status_code == 200 |
0 commit comments