Skip to content

Commit 02f1075

Browse files
committed
PEP 740 persistence, take 3
This reverts commit b6cf775. Signed-off-by: William Woodruff <[email protected]>
1 parent c7611e6 commit 02f1075

27 files changed

+991
-516
lines changed

dev/environment

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ BREACHED_EMAILS=warehouse.accounts.NullEmailBreachedService
4646
BREACHED_PASSWORDS=warehouse.accounts.NullPasswordBreachedService
4747

4848
OIDC_BACKEND=warehouse.oidc.services.NullOIDCPublisherService
49+
ATTESTATIONS_BACKEND=warehouse.attestations.services.NullIntegrityService
4950

5051
METRICS_BACKEND=warehouse.metrics.DataDogMetrics host=notdatadog
5152

requirements/main.in

+3-2
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,11 @@ requests
6060
requests-aws4auth
6161
redis>=2.8.0,<6.0.0
6262
rfc3986
63+
rfc8785
6364
sentry-sdk
6465
setuptools
65-
sigstore~=3.0.0
66-
pypi-attestations==0.0.9
66+
sigstore~=3.2.0
67+
pypi-attestations==0.0.11
6768
sqlalchemy[asyncio]>=2.0,<3.0
6869
stdlib-list
6970
stripe

requirements/main.txt

+9-7
Original file line numberDiff line numberDiff line change
@@ -1776,9 +1776,9 @@ pyparsing==3.1.4 \
17761776
--hash=sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c \
17771777
--hash=sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032
17781778
# via linehaul
1779-
pypi-attestations==0.0.9 \
1780-
--hash=sha256:3bfc07f64a8db0d6e2646720e70df7c7cb01a2936056c764a2cc3268969332f2 \
1781-
--hash=sha256:4b38cce5d221c8145cac255bfafe650ec0028d924d2b3572394df8ba8f07a609
1779+
pypi-attestations==0.0.11 \
1780+
--hash=sha256:b730e6b23874d94da0f3817b1f9dd3ecb6a80d685f62a18ad96e5b0396149ded \
1781+
--hash=sha256:e74329074f049568591e300373e12fcd46a35e21723110856546e33bf2949efa
17821782
# via -r requirements/main.in
17831783
pyqrcode==1.2.1 \
17841784
--hash=sha256:1b2812775fa6ff5c527977c4cd2ccb07051ca7d0bc0aecf937a43864abe5eff6 \
@@ -1963,7 +1963,9 @@ rfc3986==2.0.0 \
19631963
rfc8785==0.1.3 \
19641964
--hash=sha256:167efe3b5cdd09dded9d0cfc8fec1f48f5cd9f8f13b580ada4efcac138925048 \
19651965
--hash=sha256:6116062831c62e7ac5d027973a1fe07b601ccd854bca4a2b401938a00a20b0c0
1966-
# via sigstore
1966+
# via
1967+
# -r requirements/main.in
1968+
# sigstore
19671969
rich==13.8.0 \
19681970
--hash=sha256:2e85306a063b9492dffc86278197a60cbece75bcb766022f3436f567cae11bdc \
19691971
--hash=sha256:a5ac1f1cd448ade0d59cc3356f7db7a7ccda2c8cbae9c7a90c28ff463d3e91f4
@@ -2091,9 +2093,9 @@ sentry-sdk==2.13.0 \
20912093
--hash=sha256:6beede8fc2ab4043da7f69d95534e320944690680dd9a963178a49de71d726c6 \
20922094
--hash=sha256:8d4a576f7a98eb2fdb40e13106e41f330e5c79d72a68be1316e7852cf4995260
20932095
# via -r requirements/main.in
2094-
sigstore==3.0.0 \
2095-
--hash=sha256:6cc7dc92607c2fd481aada0f3c79e710e4c6086e3beab50b07daa9a50a79d109 \
2096-
--hash=sha256:a6a9538a648e112a0c3d8092d3f73a351c7598164764f1e73a6b5ba406a3a0bd
2096+
sigstore==3.2.0 \
2097+
--hash=sha256:25c8a871a3a6adf959c0cde598ea8bef8794f1a29277d067111eb4ded4ba7f65 \
2098+
--hash=sha256:d18508f34febb7775065855e92557fa1c2c16580df88f8e8903b9514438bad44
20972099
# via
20982100
# -r requirements/main.in
20992101
# pypi-attestations

tests/conftest.py

+25-2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
from jinja2 import Environment, FileSystemLoader
3232
from psycopg.errors import InvalidCatalogName
33+
from pypi_attestations import Attestation, Envelope, VerificationMaterial
3334
from pyramid.i18n import TranslationString
3435
from pyramid.static import ManifestCacheBuster
3536
from pyramid_jinja2 import IJinja2Environment
@@ -44,6 +45,8 @@
4445
from warehouse.accounts import services as account_services
4546
from warehouse.accounts.interfaces import ITokenService, IUserService
4647
from warehouse.admin.flags import AdminFlag, AdminFlagValue
48+
from warehouse.attestations import services as attestations_services
49+
from warehouse.attestations.interfaces import IIntegrityService
4750
from warehouse.email import services as email_services
4851
from warehouse.email.interfaces import IEmailSender
4952
from warehouse.helpdesk import services as helpdesk_services
@@ -173,6 +176,7 @@ def pyramid_services(
173176
project_service,
174177
github_oidc_service,
175178
activestate_oidc_service,
179+
integrity_service,
176180
macaroon_service,
177181
helpdesk_service,
178182
):
@@ -194,6 +198,7 @@ def pyramid_services(
194198
services.register_service(
195199
activestate_oidc_service, IOIDCPublisherService, None, name="activestate"
196200
)
201+
services.register_service(integrity_service, IIntegrityService, None, name="")
197202
services.register_service(macaroon_service, IMacaroonService, None, name="")
198203
services.register_service(helpdesk_service, IHelpDeskService, None)
199204

@@ -324,6 +329,7 @@ def get_app_config(database, nondefaults=None):
324329
"docs.backend": "warehouse.packaging.services.LocalDocsStorage",
325330
"sponsorlogos.backend": "warehouse.admin.services.LocalSponsorLogoStorage",
326331
"billing.backend": "warehouse.subscriptions.services.MockStripeBillingService",
332+
"attestations.backend": "warehouse.attestations.services.NullIntegrityService",
327333
"billing.api_base": "http://stripe:12111",
328334
"billing.api_version": "2020-08-27",
329335
"mail.backend": "warehouse.email.services.SMTPEmailSender",
@@ -387,13 +393,11 @@ def get_db_session_for_app_config(app_config):
387393

388394
@pytest.fixture(scope="session")
389395
def app_config(database):
390-
391396
return get_app_config(database)
392397

393398

394399
@pytest.fixture(scope="session")
395400
def app_config_dbsession_from_env(database):
396-
397401
nondefaults = {
398402
"warehouse.db_create_session": lambda r: r.environ.get("warehouse.db_session")
399403
}
@@ -539,6 +543,25 @@ def activestate_oidc_service(db_session):
539543
)
540544

541545

546+
@pytest.fixture
547+
def dummy_attestation():
548+
return Attestation(
549+
version=1,
550+
verification_material=VerificationMaterial(
551+
certificate="somebase64string", transparency_entries=[dict()]
552+
),
553+
envelope=Envelope(
554+
statement="somebase64string",
555+
signature="somebase64string",
556+
),
557+
)
558+
559+
560+
@pytest.fixture
561+
def integrity_service(db_session):
562+
return attestations_services.NullIntegrityService(db_session)
563+
564+
542565
@pytest.fixture
543566
def macaroon_service(db_session):
544567
return macaroon_services.DatabaseMacaroonService(db_session)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"version":1,"verification_material":{"certificate":"MIIC6zCCAnGgAwIBAgIUFgmhIYx8gvBGePCTacG/4kbBdRwwCgYIKoZIzj0EAwMwNzEVMBMGA1UE\nChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjQwODI5\nMTcwOTM5WhcNMjQwODI5MTcxOTM5WjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEtGrMPml4\nOtsRJ3Z6qRahs0kHCZxP4n9fvrJE957WVxgAGg4k6a1PbRJY9nT9wKpRrZmKV++AgA9ndhdruXXa\nAKOCAZAwggGMMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQU\nosNvhYEuTPfgyU/dZfu93lFGRNswHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wQAYD\nVR0RAQH/BDYwNIEyOTE5NDM2MTU4MjM2LWNvbXB1dGVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3Vu\ndC5jb20wKQYKKwYBBAGDvzABAQQbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMCsGCisGAQQB\ng78wAQgEHQwbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMIGKBgorBgEEAdZ5AgQCBHwEegB4\nAHYA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGRnx0/aQAABAMARzBFAiBogvcK\nHIIR9FcX1vQgDhGtAl0XQoMRiEB3OdUWO94P1gIhANdJlyISdtvVrHes25dWKTLepy+IzQmzfQU/\nS7cxWHmOMAoGCCqGSM49BAMDA2gAMGUCMGe2xTiuenbjdt1d2e4IaCiwRh2G4KAtyujRESSSUbpu\nGme/o9ouiApeONBv2CvvGAIxAOEkAGFO3aALE3IPNosxqaz9MbqJOdmYhB1Cz1D7xbFc/m243VxJ\nWxaC/uOFEpyiYQ==\n","transparency_entries":[{"logIndex":"125970014","logId":{"keyId":"wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="},"kindVersion":{"kind":"dsse","version":"0.0.1"},"integratedTime":"1724951379","inclusionPromise":{"signedEntryTimestamp":"MEUCIQCHrKFTeXNY432S0bUSBS69S8d5JnNcDXa41q6OEvxEwgIgaZstc5Jpm0IgwFC7RDTXYEAKk+3aG/MkRkaPdJdyn8U="},"inclusionProof":{"logIndex":"4065752","rootHash":"7jVDF3UNUZVEU85ffETQ3WKfXhOoMi4cgytJM250HTk=","treeSize":"4065754","hashes":["NwJgWJoxjearbnEIT9bnWXpzo0LGNrR1cpWId0g66rE=","kLjpW3Eh7pQJNOvyntghzF57tcfqk2IzX7cqiBDgGf8=","FW8y9LQ1i3q+MnbeGJipKGl4VfX1zRBOD7TmhbEw7uI=","mKcbGJDJ/+buNbXy9Eyv94nVoAyUauuIlN3cJg3qSBY=","5VytqqAHhfRkRWMrY43UXWCnRBb7JwElMlKpY5JueBc=","mZJnD39LTKdis2wUTz1OOMx3r7HwgJh9rnb2VwiPzts=","MXZOQFJFiOjREF0xwMOCXu29HwTchjTtl/BeFoI51wY=","g8zCkHnLwO3LojK7g5AnqE8ezSNRnCSz9nCL5GD3a8A=","RrZsD/RSxNoujlvq/MsCEvLSkKZfv0jmQM9Kp7qbJec=","QxmVWsbTp4cClxuAkuT51UH2EY7peHMVGKq7+b+cGwQ=","Q2LAtNzOUh+3PfwfMyNxYb06fTQmF3VeTT6Fr6Upvfc=","ftwAu6v62WFDoDmcZ1JKfrRPrvuiIw5v3BvRsgQj7N8="],"checkpoint":{"envelope":"rekor.sigstore.dev - 1193050959916656506\n4065754\n7jVDF3UNUZVEU85ffETQ3WKfXhOoMi4cgytJM250HTk=\n\n— rekor.sigstore.dev wNI9ajBGAiEAhMomhZHOTNB5CVPO98CMXCv01ZlIF+C+CgzraAB01r8CIQCEuXbv6aqguUpB/ig5eXRIbarvxLXkg3nX48DzambktQ==\n"}},"canonicalizedBody":"eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZW52ZWxvcGVIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiOWRiNGJjMzE3MTgyZWI3NzljNDIyY2Q0NGI2ZDdlYTk5ZWM1M2Q3M2JiY2ZjZWVmZTIyNWVlYjQ3NTQyMjc4OCJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjlkYjY0MjlhOTkzZGFiYTI4NzAwODk2ZTY2MzNjNzkxYWE0MDM3ODQ4NjJiYzY2MDBkM2E4NjYwMGQzYjA1NjMifSwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUVVQ0lCaGlOL25NR0w3aHpZQk9QQjlUTGtuaEdTZEtuQ0Q0ekI3TDV5ZXc0QmJ3QWlFQXJzOHl6MCtCT2NnSEtzS0JzTXVOeVlhREdaRTBVV0JuMEdwNVpGMzUvU2M9IiwidmVyaWZpZXIiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VNMmVrTkRRVzVIWjBGM1NVSkJaMGxWUm1kdGFFbFplRGhuZGtKSFpWQkRWR0ZqUnk4MGEySkNaRkozZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwUmQwOUVTVFZOVkdOM1QxUk5OVmRvWTA1TmFsRjNUMFJKTlUxVVkzaFBWRTAxVjJwQlFVMUdhM2RGZDFsSUNrdHZXa2w2YWpCRFFWRlpTVXR2V2tsNmFqQkVRVkZqUkZGblFVVjBSM0pOVUcxc05FOTBjMUpLTTFvMmNWSmhhSE13YTBoRFduaFFORzQ1Wm5aeVNrVUtPVFUzVjFaNFowRkhaelJyTm1FeFVHSlNTbGs1YmxRNWQwdHdVbkphYlV0V0t5dEJaMEU1Ym1Sb1pISjFXRmhoUVV0UFEwRmFRWGRuWjBkTlRVRTBSd3BCTVZWa1JIZEZRaTkzVVVWQmQwbElaMFJCVkVKblRsWklVMVZGUkVSQlMwSm5aM0pDWjBWR1FsRmpSRUY2UVdSQ1owNVdTRkUwUlVablVWVnZjMDUyQ21oWlJYVlVVR1puZVZVdlpGcG1kVGt6YkVaSFVrNXpkMGgzV1VSV1VqQnFRa0puZDBadlFWVXpPVkJ3ZWpGWmEwVmFZalZ4VG1wd1MwWlhhWGhwTkZrS1drUTRkMUZCV1VSV1VqQlNRVkZJTDBKRVdYZE9TVVY1VDFSRk5VNUVUVEpOVkZVMFRXcE5Na3hYVG5aaVdFSXhaRWRXUVZwSFZqSmFWM2gyWTBkV2VRcE1iV1I2V2xoS01tRlhUbXhaVjA1cVlqTldkV1JETldwaU1qQjNTMUZaUzB0M1dVSkNRVWRFZG5wQlFrRlJVV0poU0ZJd1kwaE5Oa3g1T1doWk1rNTJDbVJYTlRCamVUVnVZakk1Ym1KSFZYVlpNamwwVFVOelIwTnBjMGRCVVZGQ1p6YzRkMEZSWjBWSVVYZGlZVWhTTUdOSVRUWk1lVGxvV1RKT2RtUlhOVEFLWTNrMWJtSXlPVzVpUjFWMVdUSTVkRTFKUjB0Q1oyOXlRbWRGUlVGa1dqVkJaMUZEUWtoM1JXVm5RalJCU0ZsQk0xUXdkMkZ6WWtoRlZFcHFSMUkwWXdwdFYyTXpRWEZLUzFoeWFtVlFTek12YURSd2VXZERPSEEzYnpSQlFVRkhVbTU0TUM5aFVVRkJRa0ZOUVZKNlFrWkJhVUp2WjNaalMwaEpTVkk1Um1OWUNqRjJVV2RFYUVkMFFXd3dXRkZ2VFZKcFJVSXpUMlJWVjA4NU5GQXhaMGxvUVU1a1NteDVTVk5rZEhaV2NraGxjekkxWkZkTFZFeGxjSGtyU1hwUmJYb0tabEZWTDFNM1kzaFhTRzFQVFVGdlIwTkRjVWRUVFRRNVFrRk5SRUV5WjBGTlIxVkRUVWRsTW5oVWFYVmxibUpxWkhReFpESmxORWxoUTJsM1VtZ3lSd28wUzBGMGVYVnFVa1ZUVTFOVlluQjFSMjFsTDI4NWIzVnBRWEJsVDA1Q2RqSkRkblpIUVVsNFFVOUZhMEZIUms4ellVRk1SVE5KVUU1dmMzaHhZWG81Q2sxaWNVcFBaRzFaYUVJeFEzb3hSRGQ0WWtaakwyMHlORE5XZUVwWGVHRkRMM1ZQUmtWd2VXbFpVVDA5Q2kwdExTMHRSVTVFSUVORlVsUkpSa2xEUVZSRkxTMHRMUzBLIn1dfX0="}]},"envelope":{"statement":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjEiLCJzdWJqZWN0IjpbeyJu\nYW1lIjoic2FtcGxlcHJvamVjdC0zLjAuMC50YXIuZ3oiLCJkaWdlc3QiOnsic2hhMjU2IjoiMTE3\nZWQ4OGU1ZGIwNzNiYjkyOTY5YTc1NDU3NDVmZDk3N2VlODViNzAxOTcwNmRkMjU2YTY0MDU4Zjcw\nOTYzZCJ9fV0sInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2RvY3MucHlwaS5vcmcvYXR0ZXN0YXRp\nb25zL3B1Ymxpc2gvdjEiLCJwcmVkaWNhdGUiOm51bGx9\n","signature":"MEUCIBhiN/nMGL7hzYBOPB9TLknhGSdKnCD4zB7L5yew4BbwAiEArs8yz0+BOcgHKsKBsMuNyYaD\nGZE0UWBn0Gp5ZF35/Sc=\n"}}

tests/functional/api/test_simple.py

+97-1
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,19 @@
1111
# limitations under the License.
1212

1313
from http import HTTPStatus
14+
from pathlib import Path
1415

15-
from ...common.db.packaging import ProjectFactory, ReleaseFactory
16+
import pymacaroons
17+
18+
from warehouse.macaroons import caveats
19+
20+
from ...common.db.accounts import EmailFactory, UserFactory
21+
from ...common.db.macaroons import MacaroonFactory
22+
from ...common.db.oidc import GitHubPublisherFactory
23+
from ...common.db.packaging import ProjectFactory, ReleaseFactory, RoleFactory
24+
25+
_HERE = Path(__file__).parent
26+
_ASSETS = _HERE.parent / "_fixtures"
1627

1728

1829
def test_simple_api_html(webtest):
@@ -31,3 +42,88 @@ def test_simple_api_detail(webtest):
3142
assert resp.content_type == "text/html"
3243
assert "X-PyPI-Last-Serial" in resp.headers
3344
assert f"Links for {project.normalized_name}" in resp.text
45+
46+
47+
def test_simple_attestations_from_upload(webtest):
48+
user = UserFactory.create(
49+
password=( # 'password'
50+
"$argon2id$v=19$m=1024,t=6,p=6$EiLE2Nsbo9S6N+acs/beGw$ccyZDCZstr1/+Y/1s3BVZ"
51+
"HOJaqfBroT0JCieHug281c"
52+
)
53+
)
54+
EmailFactory.create(user=user, verified=True)
55+
project = ProjectFactory.create(name="sampleproject")
56+
RoleFactory.create(user=user, project=project, role_name="Owner")
57+
publisher = GitHubPublisherFactory.create(projects=[project])
58+
59+
# Construct the macaroon. This needs to be based on a Trusted Publisher, which is
60+
# required to upload attestations
61+
dm = MacaroonFactory.create(
62+
oidc_publisher_id=publisher.id,
63+
caveats=[
64+
caveats.OIDCPublisher(oidc_publisher_id=str(publisher.id)),
65+
caveats.ProjectID(project_ids=[str(p.id) for p in publisher.projects]),
66+
],
67+
additional={"oidc": {"ref": "someref", "sha": "somesha"}},
68+
)
69+
70+
m = pymacaroons.Macaroon(
71+
location="localhost",
72+
identifier=str(dm.id),
73+
key=dm.key,
74+
version=pymacaroons.MACAROON_V2,
75+
)
76+
for caveat in dm.caveats:
77+
m.add_first_party_caveat(caveats.serialize(caveat))
78+
serialized_macaroon = f"pypi-{m.serialize()}"
79+
80+
with open(_ASSETS / "sampleproject-3.0.0.tar.gz", "rb") as f:
81+
content = f.read()
82+
83+
with open(
84+
_ASSETS / "sampleproject-3.0.0.tar.gz.publish.attestation",
85+
) as f:
86+
attestation = f.read()
87+
88+
webtest.set_authorization(("Basic", ("__token__", serialized_macaroon)))
89+
webtest.post(
90+
"/legacy/?:action=file_upload",
91+
params={
92+
"name": "sampleproject",
93+
"sha256_digest": (
94+
"117ed88e5db073bb92969a7545745fd977ee85b7019706dd256a64058f70963d"
95+
),
96+
"filetype": "sdist",
97+
"metadata_version": "2.1",
98+
"version": "3.0.0",
99+
"attestations": f"[{attestation}]",
100+
},
101+
upload_files=[("content", "sampleproject-3.0.0.tar.gz", content)],
102+
status=HTTPStatus.OK,
103+
)
104+
105+
assert len(project.releases) == 1
106+
assert project.releases[0].files.count() == 1
107+
assert project.releases[0].files[0].provenance is not None
108+
109+
expected_provenance = project.releases[0].files[0].provenance.provenance_digest
110+
expected_filename = "sampleproject-3.0.0.tar.gz"
111+
112+
response = webtest.get("/simple/sampleproject/", status=HTTPStatus.OK)
113+
link = response.html.find("a", text=expected_filename)
114+
115+
assert "data-provenance" in link.attrs
116+
assert link.get("data-provenance") == expected_provenance
117+
118+
response = webtest.get(
119+
"/simple/sampleproject/",
120+
headers={"Accept": "application/vnd.pypi.simple.v1+json"},
121+
status=HTTPStatus.OK,
122+
)
123+
124+
assert response.content_type == "application/vnd.pypi.simple.v1+json"
125+
126+
json_content = response.json
127+
assert len(json_content["files"]) == 1
128+
assert json_content["files"][0]["filename"] == expected_filename
129+
assert json_content["files"][0]["provenance"] == expected_provenance

tests/unit/api/test_simple.py

+73
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from pyramid.httpexceptions import HTTPMovedPermanently
1818
from pyramid.testing import DummyRequest
1919

20+
from tests.common.db.oidc import GitHubPublisherFactory
2021
from warehouse.api import simple
2122
from warehouse.packaging.utils import API_VERSION
2223

@@ -286,6 +287,7 @@ def test_with_files_no_serial(self, db_request, content_type, renderer_override)
286287
"upload-time": f.upload_time.isoformat() + "Z",
287288
"data-dist-info-metadata": False,
288289
"core-metadata": False,
290+
"provenance": None,
289291
}
290292
for f in files
291293
],
@@ -334,6 +336,7 @@ def test_with_files_with_serial(self, db_request, content_type, renderer_overrid
334336
"upload-time": f.upload_time.isoformat() + "Z",
335337
"data-dist-info-metadata": False,
336338
"core-metadata": False,
339+
"provenance": None,
337340
}
338341
for f in files
339342
],
@@ -427,6 +430,7 @@ def test_with_files_with_version_multi_digit(
427430
if f.metadata_file_sha256_digest is not None
428431
else False
429432
),
433+
"provenance": None,
430434
}
431435
for f in files
432436
],
@@ -439,6 +443,75 @@ def test_with_files_with_version_multi_digit(
439443
if renderer_override is not None:
440444
assert db_request.override_renderer == renderer_override
441445

446+
def test_with_files_varying_provenance(
447+
self, db_request, integrity_service, dummy_attestation
448+
):
449+
db_request.oidc_publisher = GitHubPublisherFactory.create()
450+
451+
project = ProjectFactory.create()
452+
release = ReleaseFactory.create(project=project, version="1.0.0")
453+
454+
# wheel with provenance, sdist with no provenance
455+
wheel = FileFactory.create(
456+
release=release,
457+
filename=f"{project.name}-1.0.0.whl",
458+
packagetype="bdist_wheel",
459+
metadata_file_sha256_digest="deadbeefdeadbeefdeadbeefdeadbeef",
460+
)
461+
462+
provenance = integrity_service.build_provenance(
463+
db_request, wheel, [dummy_attestation]
464+
)
465+
assert wheel.provenance == provenance
466+
assert wheel.provenance.provenance_digest is not None
467+
468+
sdist = FileFactory.create(
469+
release=release,
470+
filename=f"{project.name}-1.0.0.tar.gz",
471+
packagetype="sdist",
472+
)
473+
474+
files = [sdist, wheel]
475+
476+
urls_iter = (f"/file/{f.filename}" for f in files)
477+
db_request.matchdict["name"] = project.normalized_name
478+
db_request.route_url = lambda *a, **kw: next(urls_iter)
479+
user = UserFactory.create()
480+
je = JournalEntryFactory.create(name=project.name, submitted_by=user)
481+
482+
assert simple.simple_detail(project, db_request) == {
483+
"meta": {"_last-serial": je.id, "api-version": API_VERSION},
484+
"name": project.normalized_name,
485+
"versions": ["1.0.0"],
486+
"files": [
487+
{
488+
"filename": f.filename,
489+
"url": f"/file/{f.filename}",
490+
"hashes": {"sha256": f.sha256_digest},
491+
"requires-python": f.requires_python,
492+
"yanked": False,
493+
"size": f.size,
494+
"upload-time": f.upload_time.isoformat() + "Z",
495+
"data-dist-info-metadata": (
496+
{"sha256": "deadbeefdeadbeefdeadbeefdeadbeef"}
497+
if f.metadata_file_sha256_digest is not None
498+
else False
499+
),
500+
"core-metadata": (
501+
{"sha256": "deadbeefdeadbeefdeadbeefdeadbeef"}
502+
if f.metadata_file_sha256_digest is not None
503+
else False
504+
),
505+
"provenance": (
506+
f.provenance.provenance_blake2_256_digest
507+
if f.provenance is not None
508+
else None
509+
),
510+
}
511+
for f in files
512+
],
513+
}
514+
442515
def test_with_files_quarantined_omitted_from_index(self, db_request):
443516
db_request.accept = "text/html"
444517
project = ProjectFactory.create(lifecycle_status="quarantine-enter")

0 commit comments

Comments
 (0)