Skip to content

Commit d1c1161

Browse files
woodruffwDarkaMauldi
authored
Reapply "Store attestations for PEP740 (#16302)" (#16545) (#16546)
* Reapply "Store attestations for PEP740 (#16302)" (#16545) This reverts commit da7e1ed. * migrations: re-roll migration history Signed-off-by: William Woodruff <[email protected]> * config: register .attestations for inclusion Signed-off-by: William Woodruff <[email protected]> * attestations: request the appropriate IFileStorage service IFileStorage requires a name to disambiguate it. Signed-off-by: William Woodruff <[email protected]> * conftest: add archive_files.path to get_app_config Signed-off-by: William Woodruff <[email protected]> * test, warehouse: remove problematic mocks This removes two mocked `db_request`s from the simple index tests. These mocks were masking larger architectural issues with both attestations and our test scaffolding for attestations. This isn't quite complete yet, since it does a nasty thing (uses a file storage with a tmpdir) to get IntegrityService initialization working. Signed-off-by: William Woodruff <[email protected]> * test_services: rename test class Signed-off-by: William Woodruff <[email protected]> * Try to clean a bit the mess with the migrations. * begin refactoring IntegrityService This reduces the overall API surface for IIntegrityService implementers, and adds an initial NullIntegrityService to make unit-level testing simpler. Signed-off-by: William Woodruff <[email protected]> * Revert "Try to clean a bit the mess with the migrations." This reverts commit e19be6c. * tests, warehouse: more error tests, remove more stubs Signed-off-by: William Woodruff <[email protected]> * test_services: fix match Signed-off-by: William Woodruff <[email protected]> * remove more implicit file service deps Signed-off-by: William Woodruff <[email protected]> * continue to burn down coverage Remove more ad-hoc stubs as well. Signed-off-by: William Woodruff <[email protected]> * full coverage Signed-off-by: William Woodruff <[email protected]> * test_simple: positive provenance test for /simple Signed-off-by: William Woodruff <[email protected]> * tests: minimize, increase confidence in behavior Signed-off-by: William Woodruff <[email protected]> * Update warehouse/config.py Co-authored-by: dm <[email protected]> * packaging/test_utils: remove another mock Signed-off-by: William Woodruff <[email protected]> * Remove even more mocks * Update tests/conftest.py Co-authored-by: William Woodruff <[email protected]> * Update test_create_service * Add a functional test * Linting * Fixup migration * Fix test error * Revert change * Apply suggestions from code review Rename key Co-authored-by: Dustin Ingram <[email protected]> * Revert "Apply suggestions from code review " This reverts commit 52931a1. * Give the IntegrityService access to the session * Add the Attestation object to the session * Update the functional test with more assertions * Remove vestigial helper * Update functional test --------- Signed-off-by: William Woodruff <[email protected]> Co-authored-by: Alexis <[email protected]> Co-authored-by: dm <[email protected]> Co-authored-by: Dustin Ingram <[email protected]>
1 parent 6ce90e9 commit d1c1161

29 files changed

+1352
-496
lines changed

dev/environment

Lines changed: 1 addition & 0 deletions
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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ redis>=2.8.0,<6.0.0
6262
rfc3986
6363
sentry-sdk
6464
setuptools
65-
sigstore~=3.0.0
66-
pypi-attestations==0.0.9
65+
sigstore~=3.2.0
66+
pypi-attestations==0.0.11
6767
sqlalchemy[asyncio]>=2.0,<3.0
6868
stdlib-list
6969
stripe

requirements/main.txt

Lines changed: 6 additions & 6 deletions
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 \
@@ -2091,9 +2091,9 @@ sentry-sdk==2.13.0 \
20912091
--hash=sha256:6beede8fc2ab4043da7f69d95534e320944690680dd9a963178a49de71d726c6 \
20922092
--hash=sha256:8d4a576f7a98eb2fdb40e13106e41f330e5c79d72a68be1316e7852cf4995260
20932093
# via -r requirements/main.in
2094-
sigstore==3.0.0 \
2095-
--hash=sha256:6cc7dc92607c2fd481aada0f3c79e710e4c6086e3beab50b07daa9a50a79d109 \
2096-
--hash=sha256:a6a9538a648e112a0c3d8092d3f73a351c7598164764f1e73a6b5ba406a3a0bd
2094+
sigstore==3.2.0 \
2095+
--hash=sha256:25c8a871a3a6adf959c0cde598ea8bef8794f1a29277d067111eb4ded4ba7f65 \
2096+
--hash=sha256:d18508f34febb7775065855e92557fa1c2c16580df88f8e8903b9514438bad44
20972097
# via
20982098
# -r requirements/main.in
20992099
# pypi-attestations

tests/common/db/attestation.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Licensed under the Apache License, Version 2.0 (the "License");
2+
# you may not use this file except in compliance with the License.
3+
# You may obtain a copy of the License at
4+
#
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
#
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
import hashlib
13+
14+
import factory
15+
16+
from warehouse.attestations.models import Attestation
17+
18+
from .base import WarehouseFactory
19+
20+
21+
class AttestationFactory(WarehouseFactory):
22+
class Meta:
23+
model = Attestation
24+
25+
file = factory.SubFactory("tests.common.db.packaging.FileFactory")
26+
attestation_file_blake2_digest = factory.LazyAttribute(
27+
lambda o: hashlib.blake2b(o.file.filename.encode("utf8")).hexdigest()
28+
)

tests/common/db/packaging.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from warehouse.utils import readme
3535

3636
from .accounts import UserFactory
37+
from .attestation import AttestationFactory
3738
from .base import WarehouseFactory
3839
from .observations import ObserverFactory
3940

@@ -140,6 +141,13 @@ class Meta:
140141
)
141142
)
142143

144+
# Empty attestations by default.
145+
attestations = factory.RelatedFactoryList(
146+
AttestationFactory,
147+
factory_related_name="file",
148+
size=0,
149+
)
150+
143151

144152
class FileEventFactory(WarehouseFactory):
145153
class Meta:

tests/conftest.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
from warehouse.accounts import services as account_services
4545
from warehouse.accounts.interfaces import ITokenService, IUserService
4646
from warehouse.admin.flags import AdminFlag, AdminFlagValue
47+
from warehouse.attestations import services as attestations_services
48+
from warehouse.attestations.interfaces import IIntegrityService
4749
from warehouse.email import services as email_services
4850
from warehouse.email.interfaces import IEmailSender
4951
from warehouse.helpdesk import services as helpdesk_services
@@ -57,7 +59,7 @@
5759
from warehouse.organizations import services as organization_services
5860
from warehouse.organizations.interfaces import IOrganizationService
5961
from warehouse.packaging import services as packaging_services
60-
from warehouse.packaging.interfaces import IProjectService
62+
from warehouse.packaging.interfaces import IFileStorage, IProjectService
6163
from warehouse.subscriptions import services as subscription_services
6264
from warehouse.subscriptions.interfaces import IBillingService, ISubscriptionService
6365

@@ -112,6 +114,15 @@ def metrics():
112114
)
113115

114116

117+
@pytest.fixture
118+
def storage_service(tmp_path):
119+
"""
120+
A good-enough local file storage service.
121+
"""
122+
123+
return packaging_services.LocalArchiveFileStorage(tmp_path)
124+
125+
115126
@pytest.fixture
116127
def remote_addr():
117128
return "1.2.3.4"
@@ -173,6 +184,8 @@ def pyramid_services(
173184
project_service,
174185
github_oidc_service,
175186
activestate_oidc_service,
187+
integrity_service,
188+
storage_service,
176189
macaroon_service,
177190
helpdesk_service,
178191
):
@@ -194,7 +207,9 @@ def pyramid_services(
194207
services.register_service(
195208
activestate_oidc_service, IOIDCPublisherService, None, name="activestate"
196209
)
210+
services.register_service(integrity_service, IIntegrityService, None, name="")
197211
services.register_service(macaroon_service, IMacaroonService, None, name="")
212+
services.register_service(storage_service, IFileStorage, None, name="archive")
198213
services.register_service(helpdesk_service, IHelpDeskService, None)
199214

200215
return services
@@ -324,6 +339,7 @@ def get_app_config(database, nondefaults=None):
324339
"docs.backend": "warehouse.packaging.services.LocalDocsStorage",
325340
"sponsorlogos.backend": "warehouse.admin.services.LocalSponsorLogoStorage",
326341
"billing.backend": "warehouse.subscriptions.services.MockStripeBillingService",
342+
"attestations.backend": "warehouse.attestations.services.NullIntegrityService",
327343
"billing.api_base": "http://stripe:12111",
328344
"billing.api_version": "2020-08-27",
329345
"mail.backend": "warehouse.email.services.SMTPEmailSender",
@@ -387,13 +403,11 @@ def get_db_session_for_app_config(app_config):
387403

388404
@pytest.fixture(scope="session")
389405
def app_config(database):
390-
391406
return get_app_config(database)
392407

393408

394409
@pytest.fixture(scope="session")
395410
def app_config_dbsession_from_env(database):
396-
397411
nondefaults = {
398412
"warehouse.db_create_session": lambda r: r.environ.get("warehouse.db_session")
399413
}
@@ -539,6 +553,11 @@ def activestate_oidc_service(db_session):
539553
)
540554

541555

556+
@pytest.fixture
557+
def integrity_service(db_session):
558+
return attestations_services.NullIntegrityService(db_session)
559+
560+
542561
@pytest.fixture
543562
def macaroon_service(db_session):
544563
return macaroon_services.DatabaseMacaroonService(db_session)
Lines changed: 1 addition & 0 deletions
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

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,22 @@
1010
# See the License for the specific language governing permissions and
1111
# limitations under the License.
1212

13+
import hashlib
14+
1315
from http import HTTPStatus
16+
from pathlib import Path
17+
18+
import pymacaroons
19+
20+
from warehouse.macaroons import caveats
1421

15-
from ...common.db.packaging import ProjectFactory, ReleaseFactory
22+
from ...common.db.accounts import EmailFactory, UserFactory
23+
from ...common.db.macaroons import MacaroonFactory
24+
from ...common.db.oidc import GitHubPublisherFactory
25+
from ...common.db.packaging import ProjectFactory, ReleaseFactory, RoleFactory
26+
27+
_HERE = Path(__file__).parent
28+
_ASSETS = _HERE.parent / "_fixtures"
1629

1730

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

0 commit comments

Comments
 (0)