Skip to content

Commit

Permalink
PEP 740: add provenance to simple API (#16801)
Browse files Browse the repository at this point in the history
Co-authored-by: Mike Fiedler <[email protected]>
  • Loading branch information
woodruffw and miketheman authored Oct 21, 2024
1 parent 08aedde commit e04b7bb
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 3 deletions.
28 changes: 27 additions & 1 deletion tests/functional/api/test_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@

from http import HTTPStatus

from ...common.db.packaging import FileFactory, ProjectFactory, ReleaseFactory
from ...common.db.packaging import (
FileFactory,
ProjectFactory,
ProvenanceFactory,
ReleaseFactory,
)


def test_simple_api_html(webtest):
Expand All @@ -34,3 +39,24 @@ def test_simple_api_detail(webtest):
assert resp.html.h1.string == f"Links for {project.normalized_name}"
# There should be a link for every file
assert len(resp.html.find_all("a")) == 2


def test_simple_api_has_provenance(webtest):
project = ProjectFactory.create()
release = ReleaseFactory.create(project=project)
files = FileFactory.create_batch(2, release=release, packagetype="bdist_wheel")

for file in files:
ProvenanceFactory.create(file=file)

resp = webtest.get(f"/simple/{project.normalized_name}/", status=HTTPStatus.OK)
links = resp.html.find_all("a")

for file in files:
link = next(link for link in links if link.text == file.filename)
provenance_url = link.get("data-provenance")

assert provenance_url == (
f"http://localhost/integrity/{file.release.project.normalized_name}/"
f"{file.release.version}/{file.filename}/provenance"
)
11 changes: 11 additions & 0 deletions tests/functional/forklift/test_legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,3 +329,14 @@ def test_provenance_upload(webtest):
assert len(attestations) == 1
attestation = attestations[0]
assert attestation == json.loads(attestation_contents)

# While we needed to be authenticated to upload a project, this is no longer
# required to view it.
webtest.authorization = None
expected_filename = "sampleproject-3.0.0.tar.gz"

response = webtest.get(
f"/integrity/{project.name}/3.0.0/{expected_filename}/provenance",
status=HTTPStatus.OK,
)
assert response.json == project.releases[0].files[0].provenance.provenance
125 changes: 125 additions & 0 deletions tests/unit/api/test_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from pyramid.httpexceptions import HTTPMovedPermanently
from pyramid.testing import DummyRequest

from tests.common.db.oidc import GitHubPublisherFactory
from warehouse.api import simple
from warehouse.packaging.utils import API_VERSION, _valid_simple_detail_context

Expand All @@ -26,6 +27,7 @@
FileFactory,
JournalEntryFactory,
ProjectFactory,
ProvenanceFactory,
ReleaseFactory,
)

Expand Down Expand Up @@ -298,6 +300,7 @@ def test_with_files_no_serial(self, db_request, content_type, renderer_override)
"upload-time": f.upload_time.isoformat() + "Z",
"data-dist-info-metadata": False,
"core-metadata": False,
"provenance": None,
}
for f in files
],
Expand Down Expand Up @@ -349,6 +352,7 @@ def test_with_files_with_serial(self, db_request, content_type, renderer_overrid
"upload-time": f.upload_time.isoformat() + "Z",
"data-dist-info-metadata": False,
"core-metadata": False,
"provenance": None,
}
for f in files
],
Expand Down Expand Up @@ -445,6 +449,7 @@ def test_with_files_with_version_multi_digit(
if f.metadata_file_sha256_digest is not None
else False
),
"provenance": None,
}
for f in files
],
Expand Down Expand Up @@ -489,6 +494,126 @@ def test_with_files_quarantined_omitted_from_index(
if renderer_override is not None:
assert db_request.override_renderer == renderer_override

@pytest.mark.parametrize(
("content_type", "renderer_override"),
CONTENT_TYPE_PARAMS,
)
def test_with_files_varying_provenance(
self,
db_request,
integrity_service,
dummy_attestation,
content_type,
renderer_override,
):
db_request.accept = content_type
db_request.oidc_publisher = GitHubPublisherFactory.create()

project = ProjectFactory.create()
release = ReleaseFactory.create(project=project, version="1.0.0")

# wheel with provenance, sdist with no provenance
wheel = FileFactory.create(
release=release,
filename=f"{project.name}-1.0.0.whl",
packagetype="bdist_wheel",
metadata_file_sha256_digest="deadbeefdeadbeefdeadbeefdeadbeef",
)

provenance = ProvenanceFactory.create(file=wheel)
assert wheel.provenance == provenance

sdist = FileFactory.create(
release=release,
filename=f"{project.name}-1.0.0.tar.gz",
packagetype="sdist",
)

files = [sdist, wheel]

db_request.matchdict["name"] = project.normalized_name

def route_url(route, **kw):
# Rendering the simple index calls route_url once for each file,
# and zero or one times per file depending on whether the file
# has provenance. We emulate this while testing by maintaining
# a dictionary of expected URLs for each route, which are
# pulled from as appropriate when a route_url call is made.
route_urls = {
"packaging.file": {f.path: f"/file/{f.filename}" for f in files},
"integrity.provenance": {
(
wheel.release.project.normalized_name,
wheel.release.version,
wheel.filename,
): (
f"/integrity/{wheel.release.project.normalized_name}/"
f"{wheel.release.version}/{wheel.filename}/provenance"
)
},
}

match route:
case "packaging.file":
return route_urls[route].get(kw.get("path"), "")
case "integrity.provenance":
key = (
kw.get("project_name"),
kw.get("release"),
kw.get("filename"),
)
return route_urls[route].get(key, "")
case _:
pytest.fail(f"unexpected route: {route}")

db_request.route_url = route_url

user = UserFactory.create()
je = JournalEntryFactory.create(name=project.name, submitted_by=user)

context = {
"meta": {"_last-serial": je.id, "api-version": API_VERSION},
"name": project.normalized_name,
"versions": ["1.0.0"],
"files": [
{
"filename": f.filename,
"url": f"/file/{f.filename}",
"hashes": {"sha256": f.sha256_digest},
"requires-python": f.requires_python,
"yanked": False,
"size": f.size,
"upload-time": f.upload_time.isoformat() + "Z",
"data-dist-info-metadata": (
{"sha256": "deadbeefdeadbeefdeadbeefdeadbeef"}
if f.metadata_file_sha256_digest is not None
else False
),
"core-metadata": (
{"sha256": "deadbeefdeadbeefdeadbeefdeadbeef"}
if f.metadata_file_sha256_digest is not None
else False
),
"provenance": (
(
f"/integrity/{f.release.project.normalized_name}/"
f"{f.release.version}/{f.filename}/provenance"
)
if f.provenance is not None
else None
),
}
for f in files
],
"alternate-locations": [],
}
context = _update_context(context, content_type, renderer_override)

assert simple.simple_detail(project, db_request) == context

if renderer_override is not None:
assert db_request.override_renderer == renderer_override


def _update_context(context, content_type, renderer_override):
if renderer_override != "json" or content_type in [
Expand Down
12 changes: 11 additions & 1 deletion warehouse/packaging/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from warehouse.packaging.interfaces import ISimpleStorage
from warehouse.packaging.models import File, LifecycleStatus, Project, Release

API_VERSION = "1.2"
API_VERSION = "1.3"


def _simple_index(request, serial):
Expand Down Expand Up @@ -101,6 +101,16 @@ def _simple_detail(project, request):
if file.metadata_file_sha256_digest
else False
),
"provenance": (
request.route_url(
"integrity.provenance",
project_name=project.normalized_name,
release=file.release.version,
filename=file.filename,
)
if file.provenance
else None
),
}
for file in files
],
Expand Down
2 changes: 1 addition & 1 deletion warehouse/templates/api/simple/detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
<body>
<h1>Links for {{ name }}</h1>
{% for file in files -%}
<a href="{{ file.url }}#sha256={{ file.hashes.sha256 }}" {% if file.get('requires-python') %}data-requires-python="{{ file['requires-python'] }}" {% endif %}{% if file.yanked %}data-yanked="{% if file.yanked is string %}{{ file.yanked }}{% endif %}" {% endif %}{% if file['core-metadata'] %}data-dist-info-metadata="sha256={{ file['core-metadata']['sha256'] }}" data-core-metadata="sha256={{ file['core-metadata']['sha256'] }}"{% endif %}>{{ file.filename }}</a><br />
<a href="{{ file.url }}#sha256={{ file.hashes.sha256 }}" {% if file.get('requires-python') %}data-requires-python="{{ file['requires-python'] }}" {% endif %}{% if file.yanked %}data-yanked="{% if file.yanked is string %}{{ file.yanked }}{% endif %}" {% endif %}{% if file['core-metadata'] %}data-dist-info-metadata="sha256={{ file['core-metadata']['sha256'] }}" data-core-metadata="sha256={{ file['core-metadata']['sha256'] }}"{% endif %}{% if file.get('provenance') %}data-provenance="{{ file['provenance'] }}" {% endif %}>{{ file.filename }}</a><br />
{% endfor -%}
</body>
</html>
Expand Down

0 comments on commit e04b7bb

Please sign in to comment.