Skip to content

Commit 818210a

Browse files
author
Kairo de Araujo
committed
Manage packages/project and simple details to TUF
* Adding packages After adding a package to the Warehouse database, it generates and stores the Simple Index with a request to the RSTUF backend to include the package and its simple index in TUF Metadata. * Removing package or Project Release On PyPI Management, when a user removes a file or a project release it also removes it from TUF metadata and updates the simple details index. Co-authored-by: Lukas Puehringer <[email protected]> Signed-off-by: Kairo de Araujo <[email protected]> simplify code in warehouse.tuf.targets Signed-off-by: Kairo de Araujo <[email protected]>
1 parent 4ba1dc2 commit 818210a

File tree

8 files changed

+157
-3
lines changed

8 files changed

+157
-3
lines changed

.vscode/settings.json

Lines changed: 0 additions & 3 deletions
This file was deleted.

warehouse/forklift/legacy.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
)
7070
from warehouse.packaging.tasks import sync_file_to_cache, update_bigquery_release_files
7171
from warehouse.rate_limiting.interfaces import RateLimiterException
72+
from warehouse.tuf import targets
7273
from warehouse.utils import http, readme
7374
from warehouse.utils.project import PROJECT_NAME_RE, validate_project_name
7475
from warehouse.utils.security_policy import AuthenticationMethod
@@ -1405,6 +1406,9 @@ def file_upload(request):
14051406
file_data = file_
14061407
request.db.add(file_)
14071408

1409+
# Add the project simple detail and file to TUF Metadata
1410+
task = targets.add_file(request, project, file_)
1411+
14081412
file_.record_event(
14091413
tag=EventTag.File.FileAdd,
14101414
request=request,
@@ -1420,6 +1424,7 @@ def file_upload(request):
14201424
if request.oidc_publisher
14211425
else None,
14221426
"project_id": str(project.id),
1427+
"tuf": task["data"]["task_id"],
14231428
},
14241429
)
14251430

warehouse/manage/views/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
RoleInvitationStatus,
123123
)
124124
from warehouse.rate_limiting import IRateLimiter
125+
from warehouse.tuf import targets
125126
from warehouse.utils.http import is_safe_url
126127
from warehouse.utils.paginate import paginate_url_factory
127128
from warehouse.utils.project import confirm_project, destroy_docs, remove_project
@@ -1835,17 +1836,24 @@ def delete_project_release(self):
18351836
)
18361837
)
18371838

1839+
# Delete the project release (simple detail and files) from TUF Metadata
1840+
tasks = targets.delete_release(self.request, self.release)
1841+
18381842
self.release.project.record_event(
18391843
tag=EventTag.Project.ReleaseRemove,
18401844
request=self.request,
18411845
additional={
18421846
"submitted_by": self.request.user.username,
18431847
"canonical_version": self.release.canonical_version,
1848+
"tuf": ", ".join([task["data"]["task_id"] for task in tasks]),
18441849
},
18451850
)
18461851

18471852
self.request.db.delete(self.release)
18481853

1854+
# Generate new project simple detail and add to TUF Metadata
1855+
targets.add_file(self.request, self.release.project)
1856+
18491857
self.request.session.flash(
18501858
self.request._(f"Deleted release {self.release.version!r}"), queue="success"
18511859
)
@@ -1927,6 +1935,9 @@ def _error(message):
19271935
)
19281936
)
19291937

1938+
# Delete the file and project simple detail from TUF metadata
1939+
task = targets.delete_file(self.request, self.release.project, release_file)
1940+
19301941
release_file.record_event(
19311942
tag=EventTag.File.FileRemove,
19321943
request=self.request,
@@ -1935,6 +1946,7 @@ def _error(message):
19351946
"canonical_version": self.release.canonical_version,
19361947
"filename": release_file.filename,
19371948
"project_id": str(self.release.project.id),
1949+
"tuf": task["data"]["task_id"],
19381950
},
19391951
)
19401952

@@ -1959,6 +1971,9 @@ def _error(message):
19591971

19601972
self.request.db.delete(release_file)
19611973

1974+
# Generate new project simple detail and add to TUF Metadata
1975+
targets.add_file(self.request, self.release.project)
1976+
19621977
self.request.session.flash(
19631978
f"Deleted file {release_file.filename!r}", queue="success"
19641979
)

warehouse/packaging/interfaces.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ def get_checksum(path):
4444
Return the md5 digest of the file at a given path as a lowercase string.
4545
"""
4646

47+
def get_blake2bsum(path):
48+
"""
49+
Return the blake2b digest of the file at a given path as a lowercase string.
50+
"""
51+
4752
def store(path, file_path, *, meta=None):
4853
"""
4954
Save the file located at file_path to the file storage at the location

warehouse/packaging/services.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ def get_metadata(self, path):
7474
def get_checksum(self, path):
7575
return hashlib.md5(open(os.path.join(self.base, path), "rb").read()).hexdigest()
7676

77+
def get_blake2bsum(self, path):
78+
content_hasher = hashlib.blake2b(digest_size=256 // 8)
79+
content_hasher.update(open(os.path.join(self.base, path), "rb").read())
80+
content_hash = content_hasher.hexdigest().lower()
81+
82+
return content_hash
83+
7784
def store(self, path, file_path, *, meta=None):
7885
destination = os.path.join(self.base, path)
7986
os.makedirs(os.path.dirname(destination), exist_ok=True)
@@ -195,6 +202,9 @@ def create_service(cls, context, request):
195202
prefix = request.registry.settings.get("files.prefix")
196203
return cls(bucket, prefix=prefix)
197204

205+
def get_blake2bsum(self, path):
206+
raise NotImplementedError
207+
198208

199209
class GenericS3BlobStorage(GenericBlobStorage):
200210
def get(self, path):
@@ -247,6 +257,9 @@ def create_service(cls, context, request):
247257
prefix = request.registry.settings.get("files.prefix")
248258
return cls(bucket, prefix=prefix)
249259

260+
def get_blake2bsum(self, path):
261+
raise NotImplementedError
262+
250263

251264
@implementer(IFileStorage)
252265
class S3ArchiveFileStorage(GenericS3BlobStorage):
@@ -258,6 +271,9 @@ def create_service(cls, context, request):
258271
prefix = request.registry.settings.get("archive_files.prefix")
259272
return cls(bucket, prefix=prefix)
260273

274+
def get_blake2bsum(self, path):
275+
raise NotImplementedError
276+
261277

262278
@implementer(IDocsStorage)
263279
class S3DocsStorage:
@@ -305,6 +321,20 @@ def get_metadata(self, path):
305321
def get_checksum(self, path):
306322
raise NotImplementedError
307323

324+
@google.api_core.retry.Retry(
325+
predicate=google.api_core.retry.if_exception_type(
326+
google.api_core.exceptions.ServiceUnavailable
327+
)
328+
)
329+
def get_blake2bsum(self, path):
330+
path = self._get_path(path)
331+
blob = self.bucket.blob(path)
332+
content_hasher = hashlib.blake2b(digest_size=256 // 8)
333+
content_hasher.update(blob.download_as_string())
334+
content_hash = content_hasher.hexdigest().lower()
335+
336+
return content_hash
337+
308338
@google.api_core.retry.Retry(
309339
predicate=google.api_core.retry.if_exception_type(
310340
google.api_core.exceptions.ServiceUnavailable

warehouse/packaging/utils.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,13 @@ def render_simple_detail(project, request, store=False):
121121
)
122122

123123
return (content_hash, simple_detail_path, simple_detail_size)
124+
125+
126+
def current_simple_details_path(request, project):
127+
storage = request.find_service(ISimpleStorage)
128+
current_hash = storage.get_blake2bsum(f"{project.normalized_name}/index.html")
129+
simple_detail_path = (
130+
f"{project.normalized_name}/{current_hash}.{project.normalized_name}.html"
131+
)
132+
133+
return simple_detail_path

warehouse/tuf/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@
1414
def includeme(config):
1515
api_base_url = config.registry.settings["tuf.api.url"]
1616
config.add_settings({"tuf.api.task.url": f"{api_base_url}task/"})
17+
config.add_settings({"tuf.api.targets.url": f"{api_base_url}targets/"})
1718
config.add_settings({"tuf.api.publish.url": f"{api_base_url}targets/publish/"})

warehouse/tuf/targets.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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+
13+
import requests
14+
15+
from pyramid.httpexceptions import HTTPBadGateway
16+
17+
from warehouse.packaging.models import File
18+
from warehouse.packaging.utils import current_simple_details_path, render_simple_detail
19+
20+
21+
def _payload(targets):
22+
"""Helper to create payload for POST or DELETE targets request."""
23+
return {
24+
"targets": targets,
25+
"publish_targets": True,
26+
}
27+
28+
29+
def _payload_targets_part(path, size, digest):
30+
"""Helper to create payload part for POST targets request."""
31+
return {
32+
"path": path,
33+
"info": {
34+
"length": size,
35+
"hashes": {"blake2b-256": digest},
36+
},
37+
}
38+
39+
40+
def _handle(response):
41+
"""Helper to handle http response for POST or DELETE targets request."""
42+
if response.status_code != 202:
43+
raise HTTPBadGateway(f"Unexpected TUF Server response: {response.text}")
44+
45+
return response.json()
46+
47+
48+
def add_file(request, project, file=None):
49+
"""Call RSTUF to add file and new project simple index to TUF targets metadata.
50+
51+
NOTE: If called without file, only adds new project simple index. This
52+
can be used to re-add project simple index, after deleting a file.
53+
"""
54+
targets = []
55+
digest, path, size = render_simple_detail(project, request, store=True)
56+
simple_index_part = _payload_targets_part(path, size, digest)
57+
targets.append(simple_index_part)
58+
if file:
59+
file_part = _payload_targets_part(file.path, file.size, file.blake2_256_digest)
60+
targets.append(file_part)
61+
62+
response = requests.post(
63+
request.registry.settings["tuf.api.targets.url"], json=_payload(targets)
64+
)
65+
66+
return _handle(response)
67+
68+
69+
def delete_file(request, project, file):
70+
"""Call RSTUF to remove file and project simple index from TUF targets metadata.
71+
72+
NOTE: Simple index needs to be added separately.
73+
"""
74+
index_path = current_simple_details_path(request, project)
75+
targets = [file.path, index_path]
76+
77+
response = requests.delete(
78+
request.registry.settings["tuf.api.targets.url"], json=_payload(targets)
79+
)
80+
81+
return _handle(response)
82+
83+
84+
def delete_release(request, release):
85+
files = request.db.query(File).filter(File.release_id == release.id).all()
86+
87+
tasks = []
88+
for file in files:
89+
tasks.append(delete_file(request, release.project, file))
90+
91+
return tasks

0 commit comments

Comments
 (0)