|
11 | 11 | # limitations under the License.
|
12 | 12 |
|
13 | 13 | import base64
|
| 14 | +import json |
14 | 15 |
|
15 | 16 | from http import HTTPStatus
|
| 17 | +from pathlib import Path |
16 | 18 |
|
17 | 19 | import pymacaroons
|
18 | 20 | import pytest
|
19 | 21 |
|
20 | 22 | from webob.multidict import MultiDict
|
21 | 23 |
|
| 24 | +from tests.common.db.oidc import GitHubPublisherFactory |
| 25 | +from tests.common.db.packaging import ProjectFactory, RoleFactory |
22 | 26 | from warehouse.macaroons import caveats
|
23 | 27 |
|
24 | 28 | from ...common.db.accounts import UserFactory
|
25 | 29 | from ...common.db.macaroons import MacaroonFactory
|
26 | 30 |
|
| 31 | +_HERE = Path(__file__).parent |
| 32 | +_ASSETS = _HERE.parent / "_fixtures" |
| 33 | +assert _ASSETS.is_dir() |
| 34 | + |
27 | 35 |
|
28 | 36 | def test_incorrect_post_redirect(webtest):
|
29 | 37 | """
|
@@ -68,13 +76,7 @@ def test_remove_doc_upload(webtest):
|
68 | 76 | ],
|
69 | 77 | )
|
70 | 78 | def test_file_upload(webtest, upload_url, additional_data):
|
71 |
| - user = UserFactory.create( |
72 |
| - with_verified_primary_email=True, |
73 |
| - password=( # 'password' |
74 |
| - "$argon2id$v=19$m=1024,t=6,p=6$EiLE2Nsbo9S6N+acs/beGw$ccyZDCZstr1/+Y/1s3BVZ" |
75 |
| - "HOJaqfBroT0JCieHug281c" |
76 |
| - ), |
77 |
| - ) |
| 79 | + user = UserFactory.create(with_verified_primary_email=True, clear_pwd="password") |
78 | 80 |
|
79 | 81 | # Construct the macaroon
|
80 | 82 | dm = MacaroonFactory.create(
|
@@ -135,13 +137,7 @@ def test_file_upload(webtest, upload_url, additional_data):
|
135 | 137 |
|
136 | 138 |
|
137 | 139 | def test_duplicate_file_upload_error(webtest):
|
138 |
| - user = UserFactory.create( |
139 |
| - with_verified_primary_email=True, |
140 |
| - password=( # 'password' |
141 |
| - "$argon2id$v=19$m=1024,t=6,p=6$EiLE2Nsbo9S6N+acs/beGw$ccyZDCZstr1/+Y/1s3BVZ" |
142 |
| - "HOJaqfBroT0JCieHug281c" |
143 |
| - ), |
144 |
| - ) |
| 140 | + user = UserFactory.create(with_verified_primary_email=True, clear_pwd="password") |
145 | 141 |
|
146 | 142 | # Construct the macaroon
|
147 | 143 | dm = MacaroonFactory.create(
|
@@ -215,13 +211,7 @@ def test_duplicate_file_upload_error(webtest):
|
215 | 211 |
|
216 | 212 |
|
217 | 213 | def test_invalid_classifier_upload_error(webtest):
|
218 |
| - user = UserFactory.create( |
219 |
| - with_verified_primary_email=True, |
220 |
| - password=( # 'password' |
221 |
| - "$argon2id$v=19$m=1024,t=6,p=6$EiLE2Nsbo9S6N+acs/beGw$ccyZDCZstr1/+Y/1s3BVZ" |
222 |
| - "HOJaqfBroT0JCieHug281c" |
223 |
| - ), |
224 |
| - ) |
| 214 | + user = UserFactory.create(with_verified_primary_email=True, clear_pwd="password") |
225 | 215 |
|
226 | 216 | # Construct the macaroon
|
227 | 217 | dm = MacaroonFactory.create(
|
@@ -270,3 +260,72 @@ def test_invalid_classifier_upload_error(webtest):
|
270 | 260 | status=HTTPStatus.BAD_REQUEST,
|
271 | 261 | )
|
272 | 262 | assert "'This :: Is :: Invalid' is not a valid classifier" in resp.body.decode()
|
| 263 | + |
| 264 | + |
| 265 | +def test_provenance_upload(webtest): |
| 266 | + user = UserFactory.create(with_verified_primary_email=True, clear_pwd="password") |
| 267 | + project = ProjectFactory.create(name="sampleproject") |
| 268 | + RoleFactory.create(user=user, project=project, role_name="Owner") |
| 269 | + publisher = GitHubPublisherFactory.create(projects=[project]) |
| 270 | + |
| 271 | + # Construct the macaroon. This needs to be based on a Trusted Publisher, which is |
| 272 | + # required to upload attestations |
| 273 | + dm = MacaroonFactory.create( |
| 274 | + oidc_publisher_id=publisher.id, |
| 275 | + caveats=[ |
| 276 | + caveats.OIDCPublisher(oidc_publisher_id=str(publisher.id)), |
| 277 | + caveats.ProjectID(project_ids=[str(p.id) for p in publisher.projects]), |
| 278 | + ], |
| 279 | + additional={"oidc": {"ref": "someref", "sha": "somesha"}}, |
| 280 | + ) |
| 281 | + |
| 282 | + m = pymacaroons.Macaroon( |
| 283 | + location="localhost", |
| 284 | + identifier=str(dm.id), |
| 285 | + key=dm.key, |
| 286 | + version=pymacaroons.MACAROON_V2, |
| 287 | + ) |
| 288 | + for caveat in dm.caveats: |
| 289 | + m.add_first_party_caveat(caveats.serialize(caveat)) |
| 290 | + serialized_macaroon = f"pypi-{m.serialize()}" |
| 291 | + |
| 292 | + with open(_ASSETS / "sampleproject-3.0.0.tar.gz", "rb") as f: |
| 293 | + content = f.read() |
| 294 | + |
| 295 | + with open( |
| 296 | + _ASSETS / "sampleproject-3.0.0.tar.gz.publish.attestation", |
| 297 | + ) as f: |
| 298 | + attestation_contents = f.read() |
| 299 | + |
| 300 | + webtest.set_authorization(("Basic", ("__token__", serialized_macaroon))) |
| 301 | + webtest.post( |
| 302 | + "/legacy/?:action=file_upload", |
| 303 | + params={ |
| 304 | + "name": "sampleproject", |
| 305 | + "sha256_digest": ( |
| 306 | + "117ed88e5db073bb92969a7545745fd977ee85b7019706dd256a64058f70963d" |
| 307 | + ), |
| 308 | + "filetype": "sdist", |
| 309 | + "metadata_version": "2.1", |
| 310 | + "version": "3.0.0", |
| 311 | + "attestations": f"[{attestation_contents}]", |
| 312 | + }, |
| 313 | + upload_files=[("content", "sampleproject-3.0.0.tar.gz", content)], |
| 314 | + status=HTTPStatus.OK, |
| 315 | + ) |
| 316 | + |
| 317 | + assert len(project.releases) == 1 |
| 318 | + release = project.releases[0] |
| 319 | + assert release.files.count() == 1 |
| 320 | + file_ = project.releases[0].files[0] |
| 321 | + assert file_.provenance is not None |
| 322 | + provenance = file_.provenance.provenance |
| 323 | + assert "attestation_bundles" in provenance |
| 324 | + attestation_bundles = provenance["attestation_bundles"] |
| 325 | + assert len(attestation_bundles) == 1 |
| 326 | + bundle = provenance["attestation_bundles"][0] |
| 327 | + assert "attestations" in bundle |
| 328 | + attestations = bundle["attestations"] |
| 329 | + assert len(attestations) == 1 |
| 330 | + attestation = attestations[0] |
| 331 | + assert attestation == json.loads(attestation_contents) |
0 commit comments