pulp-python 3.20.1__tar.gz → 3.22.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pulp_python-3.20.1 → pulp_python-3.22.0}/CHANGES.md +50 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/MANIFEST.in +1 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/PKG-INFO +5 -4
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/__init__.py +1 -1
- pulp_python-3.22.0/pulp_python/app/migrations/0017_pythonpackagecontent_size.py +50 -0
- pulp_python-3.22.0/pulp_python/app/migrations/0018_packageprovenance.py +59 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/models.py +47 -1
- pulp_python-3.22.0/pulp_python/app/provenance.py +71 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/pypi/serializers.py +47 -5
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/pypi/views.py +92 -26
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/serializers.py +151 -1
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/tasks/__init__.py +1 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/tasks/sync.py +43 -6
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/tasks/upload.py +60 -13
- pulp_python-3.22.0/pulp_python/app/tasks/vulnerability_report.py +30 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/urls.py +12 -1
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/utils.py +65 -7
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/viewsets.py +63 -2
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/pytest_plugin.py +31 -1
- pulp_python-3.22.0/pulp_python/tests/functional/api/test_attestations.py +242 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_pypi_apis.py +5 -30
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_pypi_simple_json_api.py +7 -2
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_sync.py +12 -0
- pulp_python-3.22.0/pulp_python/tests/functional/api/test_upload.py +124 -0
- pulp_python-3.22.0/pulp_python/tests/functional/api/test_vulnerability_report.py +48 -0
- pulp_python-3.22.0/pulp_python/tests/functional/assets/shelf-reader-0.1.tar.gz.publish.attestation +1 -0
- pulp_python-3.22.0/pulp_python/tests/functional/assets/shelf_reader-0.1-py2-none-any.whl.publish.attestation +1 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/constants.py +13 -5
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python.egg-info/PKG-INFO +5 -4
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python.egg-info/SOURCES.txt +8 -0
- pulp_python-3.22.0/pulp_python.egg-info/requires.txt +5 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pyproject.toml +6 -5
- pulp_python-3.20.1/pulp_python/tests/functional/api/test_upload.py +0 -44
- pulp_python-3.20.1/pulp_python.egg-info/requires.txt +0 -4
- {pulp_python-3.20.1 → pulp_python-3.22.0}/COMMITMENT +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/COPYRIGHT +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/LICENSE +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/README.md +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/functest_requirements.txt +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/__init__.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/global_access_conditions.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/management/__init__.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/management/commands/__init__.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/management/commands/repair-python-metadata.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0001_initial.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0001_squashed_0010_update_json_field.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0002_pythonpackagecontent_python_version.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0003_new_sync_filters.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0004_DATA_swap_distribution_model.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0005_pythonpackagecontent_sha256.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0006_pythonrepository_autopublish.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0007_pythonpackagecontent_mv-2-1.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0008_pythonpackagecontent_unique_sha256.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0009_pythondistribution_allow_uploads.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0010_update_json_field.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0011_alter_pythondistribution_distribution_ptr_and_more.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0012_add_domain.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0013_add_rbac_permissions.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0014_pythonpackagecontent_dynamic_and_more.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0015_alter_pythonpackagecontent_options.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/0016_pythonpackagecontent_metadata_sha256.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/migrations/__init__.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/modelresource.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/pypi/__init__.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/replica.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/settings.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/tasks/publish.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/tasks/repair.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/webserver_snippets/__init__.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/webserver_snippets/apache.conf +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/app/webserver_snippets/nginx.conf +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/__init__.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/__init__.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/__init__.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_auto_publish.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_consume_content.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_crud_content_unit.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_crud_publications.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_crud_remotes.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_domains.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_download_content.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_export_import.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_full_mirror.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_rbac.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/api/test_repair.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/functional/utils.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/unit/__init__.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python/tests/unit/test_models.py +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python.egg-info/dependency_links.txt +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python.egg-info/entry_points.txt +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/pulp_python.egg-info/top_level.txt +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/setup.cfg +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/test_requirements.txt +0 -0
- {pulp_python-3.20.1 → pulp_python-3.22.0}/unittest_requirements.txt +0 -0
|
@@ -8,6 +8,38 @@
|
|
|
8
8
|
|
|
9
9
|
[//]: # (towncrier release notes start)
|
|
10
10
|
|
|
11
|
+
## 3.22.0 (2025-12-09) {: #3.22.0 }
|
|
12
|
+
|
|
13
|
+
#### Features {: #3.22.0-feature }
|
|
14
|
+
|
|
15
|
+
- Added attestations field to package upload that will create a PEP 740 Provenance object for that content.
|
|
16
|
+
[#984](https://github.com/pulp/pulp_python/issues/984)
|
|
17
|
+
- Added the ability to upload PEP 740 Provenance files to repositories.
|
|
18
|
+
|
|
19
|
+
#### Bugfixes {: #3.22.0-bugfix }
|
|
20
|
+
|
|
21
|
+
- Added some sanity validation checks to twine upload endpoint.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 3.21.0 (2025-11-18) {: #3.21.0 }
|
|
26
|
+
|
|
27
|
+
#### Features {: #3.21.0-feature }
|
|
28
|
+
|
|
29
|
+
- Added ability to serve a specific repository version of a PyPI index.
|
|
30
|
+
[#982](https://github.com/pulp/pulp_python/issues/982)
|
|
31
|
+
- Implemented PEP 700 support, adding `versions`, `size` and `upload-time` to the Simple JSON API.
|
|
32
|
+
[#996](https://github.com/pulp/pulp_python/issues/996)
|
|
33
|
+
- Added the new /scan endpoint to the RepositoryVersion viewset to generate vulnerability reports.
|
|
34
|
+
[#1012](https://github.com/pulp/pulp_python/issues/1012)
|
|
35
|
+
|
|
36
|
+
#### Bugfixes {: #3.21.0-bugfix }
|
|
37
|
+
|
|
38
|
+
- Fixed pull-through caching not checking the repository if package was not present on remote.
|
|
39
|
+
[#1004](https://github.com/pulp/pulp_python/issues/1004)
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
11
43
|
## 3.20.1 (2025-11-18) {: #3.20.1 }
|
|
12
44
|
|
|
13
45
|
#### Bugfixes {: #3.20.1-bugfix }
|
|
@@ -205,6 +237,12 @@ No significant changes.
|
|
|
205
237
|
|
|
206
238
|
---
|
|
207
239
|
|
|
240
|
+
## 3.12.8 (2025-11-18) {: #3.12.8 }
|
|
241
|
+
|
|
242
|
+
No significant changes.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
208
246
|
## 3.12.7 (2025-07-23) {: #3.12.7 }
|
|
209
247
|
|
|
210
248
|
No significant changes.
|
|
@@ -291,6 +329,12 @@ No significant changes.
|
|
|
291
329
|
|
|
292
330
|
---
|
|
293
331
|
|
|
332
|
+
## 3.11.7 (2025-11-18) {: #3.11.7 }
|
|
333
|
+
|
|
334
|
+
No significant changes.
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
294
338
|
## 3.11.6 (2025-07-23) {: #3.11.6 }
|
|
295
339
|
|
|
296
340
|
No significant changes.
|
|
@@ -355,6 +399,12 @@ No significant changes.
|
|
|
355
399
|
|
|
356
400
|
---
|
|
357
401
|
|
|
402
|
+
## 3.10.2 (2025-11-18) {: #3.10.2 }
|
|
403
|
+
|
|
404
|
+
No significant changes
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
358
408
|
## 3.10.1 (2025-07-23) {: #3.10.1 }
|
|
359
409
|
|
|
360
410
|
### Bugfixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pulp-python
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.22.0
|
|
4
4
|
Summary: pulp-python plugin for the Pulp Project
|
|
5
5
|
Author-email: Pulp Team <pulp-list@redhat.com>
|
|
6
6
|
Project-URL: Homepage, https://pulpproject.org
|
|
@@ -20,10 +20,11 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
20
20
|
Requires-Python: >=3.11
|
|
21
21
|
Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
|
23
|
-
Requires-Dist: pulpcore<3.100,>=3.85.
|
|
23
|
+
Requires-Dist: pulpcore<3.100,>=3.85.3
|
|
24
24
|
Requires-Dist: pkginfo<1.13.0,>=1.12.0
|
|
25
|
-
Requires-Dist: bandersnatch<6.
|
|
26
|
-
Requires-Dist: pypi-simple<2.0,>=1.
|
|
25
|
+
Requires-Dist: bandersnatch<6.7,>=6.6.0
|
|
26
|
+
Requires-Dist: pypi-simple<2.0,>=1.8.0
|
|
27
|
+
Requires-Dist: pypi-attestations==0.0.28
|
|
27
28
|
Dynamic: license-file
|
|
28
29
|
|
|
29
30
|
# pulp_python
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Generated by Django 4.2.26 on 2025-11-11 21:43
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models, transaction
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def add_size_to_current_models(apps, schema_editor):
|
|
7
|
+
"""Adds the size to current PythonPackageContent models."""
|
|
8
|
+
PythonPackageContent = apps.get_model("python", "PythonPackageContent")
|
|
9
|
+
RemoteArtifact = apps.get_model("core", "RemoteArtifact")
|
|
10
|
+
package_bulk = []
|
|
11
|
+
for python_package in PythonPackageContent.objects.only("pk", "size").iterator():
|
|
12
|
+
content_artifact = python_package.contentartifact_set.first()
|
|
13
|
+
if content_artifact.artifact:
|
|
14
|
+
artifact = content_artifact.artifact
|
|
15
|
+
else:
|
|
16
|
+
artifact = RemoteArtifact.objects.filter(content_artifact=content_artifact).first()
|
|
17
|
+
python_package.size = artifact.size or 0
|
|
18
|
+
package_bulk.append(python_package)
|
|
19
|
+
if len(package_bulk) == 100000:
|
|
20
|
+
with transaction.atomic():
|
|
21
|
+
PythonPackageContent.objects.bulk_update(
|
|
22
|
+
package_bulk,
|
|
23
|
+
[
|
|
24
|
+
"size",
|
|
25
|
+
],
|
|
26
|
+
)
|
|
27
|
+
package_bulk = []
|
|
28
|
+
with transaction.atomic():
|
|
29
|
+
PythonPackageContent.objects.bulk_update(
|
|
30
|
+
package_bulk,
|
|
31
|
+
[
|
|
32
|
+
"size",
|
|
33
|
+
],
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Migration(migrations.Migration):
|
|
38
|
+
|
|
39
|
+
dependencies = [
|
|
40
|
+
("python", "0016_pythonpackagecontent_metadata_sha256"),
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
operations = [
|
|
44
|
+
migrations.AddField(
|
|
45
|
+
model_name="pythonpackagecontent",
|
|
46
|
+
name="size",
|
|
47
|
+
field=models.BigIntegerField(default=0),
|
|
48
|
+
),
|
|
49
|
+
migrations.RunPython(add_size_to_current_models, migrations.RunPython.noop, elidable=True),
|
|
50
|
+
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Generated by Django 4.2.26 on 2025-12-01 19:49
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
import django.db.models.deletion
|
|
5
|
+
import pulpcore.app.util
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
|
|
10
|
+
dependencies = [
|
|
11
|
+
("python", "0017_pythonpackagecontent_size"),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.CreateModel(
|
|
16
|
+
name="PackageProvenance",
|
|
17
|
+
fields=[
|
|
18
|
+
(
|
|
19
|
+
"content_ptr",
|
|
20
|
+
models.OneToOneField(
|
|
21
|
+
auto_created=True,
|
|
22
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
23
|
+
parent_link=True,
|
|
24
|
+
primary_key=True,
|
|
25
|
+
serialize=False,
|
|
26
|
+
to="core.content",
|
|
27
|
+
),
|
|
28
|
+
),
|
|
29
|
+
("provenance", models.JSONField()),
|
|
30
|
+
("sha256", models.CharField(max_length=64)),
|
|
31
|
+
(
|
|
32
|
+
"_pulp_domain",
|
|
33
|
+
models.ForeignKey(
|
|
34
|
+
default=pulpcore.app.util.get_domain_pk,
|
|
35
|
+
on_delete=django.db.models.deletion.PROTECT,
|
|
36
|
+
to="core.domain",
|
|
37
|
+
),
|
|
38
|
+
),
|
|
39
|
+
(
|
|
40
|
+
"package",
|
|
41
|
+
models.ForeignKey(
|
|
42
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
43
|
+
related_name="provenances",
|
|
44
|
+
to="python.pythonpackagecontent",
|
|
45
|
+
),
|
|
46
|
+
),
|
|
47
|
+
],
|
|
48
|
+
options={
|
|
49
|
+
"default_related_name": "%(app_label)s_%(model_name)s",
|
|
50
|
+
"unique_together": {("sha256", "_pulp_domain")},
|
|
51
|
+
},
|
|
52
|
+
bases=("core.content",),
|
|
53
|
+
),
|
|
54
|
+
migrations.AddField(
|
|
55
|
+
model_name="pythonremote",
|
|
56
|
+
name="provenance",
|
|
57
|
+
field=models.BooleanField(default=False),
|
|
58
|
+
),
|
|
59
|
+
]
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
1
3
|
from logging import getLogger
|
|
2
4
|
|
|
3
5
|
from aiohttp.web import json_response
|
|
@@ -5,6 +7,10 @@ from django.contrib.postgres.fields import ArrayField
|
|
|
5
7
|
from django.core.exceptions import ObjectDoesNotExist
|
|
6
8
|
from django.db import models
|
|
7
9
|
from django.conf import settings
|
|
10
|
+
from django_lifecycle import (
|
|
11
|
+
BEFORE_SAVE,
|
|
12
|
+
hook,
|
|
13
|
+
)
|
|
8
14
|
from pulpcore.plugin.models import (
|
|
9
15
|
AutoAddObjPermsMixin,
|
|
10
16
|
Content,
|
|
@@ -16,6 +22,7 @@ from pulpcore.plugin.models import (
|
|
|
16
22
|
from pulpcore.plugin.responses import ArtifactResponse
|
|
17
23
|
|
|
18
24
|
from pathlib import PurePath
|
|
25
|
+
from .provenance import Provenance
|
|
19
26
|
from .utils import (
|
|
20
27
|
artifact_to_python_content_data,
|
|
21
28
|
canonicalize_name,
|
|
@@ -193,6 +200,7 @@ class PythonPackageContent(Content):
|
|
|
193
200
|
python_version = models.TextField()
|
|
194
201
|
sha256 = models.CharField(db_index=True, max_length=64)
|
|
195
202
|
metadata_sha256 = models.CharField(max_length=64, null=True)
|
|
203
|
+
size = models.BigIntegerField(default=0)
|
|
196
204
|
# yanked and yanked_reason are not implemented because they are mutable
|
|
197
205
|
|
|
198
206
|
# From pulpcore
|
|
@@ -234,6 +242,43 @@ class PythonPackageContent(Content):
|
|
|
234
242
|
]
|
|
235
243
|
|
|
236
244
|
|
|
245
|
+
class PackageProvenance(Content):
|
|
246
|
+
"""
|
|
247
|
+
PEP 740 provenance objects.
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
TYPE = "provenance"
|
|
251
|
+
repo_key_fields = ("package_id",)
|
|
252
|
+
|
|
253
|
+
package = models.ForeignKey(
|
|
254
|
+
PythonPackageContent, on_delete=models.CASCADE, related_name="provenances"
|
|
255
|
+
)
|
|
256
|
+
provenance = models.JSONField(null=False)
|
|
257
|
+
sha256 = models.CharField(max_length=64, null=False)
|
|
258
|
+
|
|
259
|
+
_pulp_domain = models.ForeignKey("core.Domain", default=get_domain_pk, on_delete=models.PROTECT)
|
|
260
|
+
|
|
261
|
+
@staticmethod
|
|
262
|
+
def calculate_sha256(provenance):
|
|
263
|
+
"""Calculates the sha256 from the provenance."""
|
|
264
|
+
provenance_json = json.dumps(provenance, sort_keys=True).encode("utf-8")
|
|
265
|
+
hasher = hashlib.sha256(provenance_json)
|
|
266
|
+
return hasher.hexdigest()
|
|
267
|
+
|
|
268
|
+
@hook(BEFORE_SAVE)
|
|
269
|
+
def set_sha256_hook(self):
|
|
270
|
+
"""Ensure that sha256 is set before saving."""
|
|
271
|
+
self.sha256 = self.calculate_sha256(self.provenance)
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def as_model(self):
|
|
275
|
+
return Provenance.model_validate(self.provenance)
|
|
276
|
+
|
|
277
|
+
class Meta:
|
|
278
|
+
default_related_name = "%(app_label)s_%(model_name)s"
|
|
279
|
+
unique_together = ("sha256", "_pulp_domain")
|
|
280
|
+
|
|
281
|
+
|
|
237
282
|
class PythonPublication(Publication, AutoAddObjPermsMixin):
|
|
238
283
|
"""
|
|
239
284
|
A Publication for PythonContent.
|
|
@@ -269,6 +314,7 @@ class PythonRemote(Remote, AutoAddObjPermsMixin):
|
|
|
269
314
|
exclude_platforms = ArrayField(
|
|
270
315
|
models.CharField(max_length=10, blank=True), choices=PLATFORMS, default=list
|
|
271
316
|
)
|
|
317
|
+
provenance = models.BooleanField(default=False)
|
|
272
318
|
|
|
273
319
|
def get_remote_artifact_url(self, relative_path=None, request=None):
|
|
274
320
|
"""Get url for remote_artifact"""
|
|
@@ -294,7 +340,7 @@ class PythonRepository(Repository, AutoAddObjPermsMixin):
|
|
|
294
340
|
"""
|
|
295
341
|
|
|
296
342
|
TYPE = "python"
|
|
297
|
-
CONTENT_TYPES = [PythonPackageContent]
|
|
343
|
+
CONTENT_TYPES = [PythonPackageContent, PackageProvenance]
|
|
298
344
|
REMOTE_TYPES = [PythonRemote]
|
|
299
345
|
PULL_THROUGH_SUPPORTED = True
|
|
300
346
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from typing import Annotated, Literal, Union, get_args
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
4
|
+
from pydantic.alias_generators import to_snake
|
|
5
|
+
from pypi_attestations import (
|
|
6
|
+
Attestation,
|
|
7
|
+
Distribution,
|
|
8
|
+
Publisher,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _PermissivePolicy:
|
|
13
|
+
"""A permissive verification policy that always succeeds."""
|
|
14
|
+
|
|
15
|
+
def verify(self, cert):
|
|
16
|
+
"""Succeed regardless of the publisher's identity."""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AnyPublisher(BaseModel):
|
|
21
|
+
"""A fallback publisher for any kind not matching other publisher types."""
|
|
22
|
+
|
|
23
|
+
model_config = ConfigDict(alias_generator=to_snake, extra="allow")
|
|
24
|
+
|
|
25
|
+
kind: str
|
|
26
|
+
|
|
27
|
+
def _as_policy(self):
|
|
28
|
+
"""Return a permissive policy that always succeed."""
|
|
29
|
+
return _PermissivePolicy()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Get the underlying Union type of the original Publisher
|
|
33
|
+
# Publisher is Annotated[Union[...], Field(discriminator="kind")]
|
|
34
|
+
_OriginalPublisherTypes = get_args(Publisher.__origin__)
|
|
35
|
+
# Add AnyPublisher to the list of original publisher types
|
|
36
|
+
_ExtendedPublisherTypes = (*_OriginalPublisherTypes, AnyPublisher)
|
|
37
|
+
_ExtendedPublisherUnion = Union[_ExtendedPublisherTypes]
|
|
38
|
+
# Create a new type that fallbacks to AnyPublisher
|
|
39
|
+
ExtendedPublisher = Annotated[_ExtendedPublisherUnion, Field(union_mode="left_to_right")]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AttestationBundle(BaseModel):
|
|
43
|
+
"""
|
|
44
|
+
AttestationBundle object as defined in PEP740.
|
|
45
|
+
|
|
46
|
+
PyPI only accepts attestations from TrustedPublishers (GitHub, GitLab, Google), but we will
|
|
47
|
+
accept from any user.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
publisher: ExtendedPublisher
|
|
51
|
+
attestations: list[Attestation]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Provenance(BaseModel):
|
|
55
|
+
"""Provenance object as defined in PEP740."""
|
|
56
|
+
|
|
57
|
+
version: Literal[1] = 1
|
|
58
|
+
attestation_bundles: list[AttestationBundle]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def verify_provenance(filename, sha256, provenance, offline=False):
|
|
62
|
+
"""Verify the provenance object is valid for the package."""
|
|
63
|
+
dist = Distribution(name=filename, digest=sha256)
|
|
64
|
+
for bundle in provenance.attestation_bundles:
|
|
65
|
+
publisher = bundle.publisher
|
|
66
|
+
policy = publisher._as_policy()
|
|
67
|
+
for attestation in bundle.attestations:
|
|
68
|
+
sig_bundle = attestation.to_bundle()
|
|
69
|
+
checkpoint = sig_bundle.log_entry._inner.inclusion_proof.checkpoint
|
|
70
|
+
staging = "sigstage.dev" in checkpoint.envelope
|
|
71
|
+
attestation.verify(policy, dist, staging=staging, offline=offline)
|
|
@@ -2,7 +2,9 @@ import logging
|
|
|
2
2
|
from gettext import gettext as _
|
|
3
3
|
|
|
4
4
|
from rest_framework import serializers
|
|
5
|
-
from
|
|
5
|
+
from pydantic import TypeAdapter, ValidationError
|
|
6
|
+
from pulp_python.app.provenance import Attestation
|
|
7
|
+
from pulp_python.app.utils import DIST_EXTENSIONS, SUPPORTED_METADATA_VERSIONS
|
|
6
8
|
from pulpcore.plugin.models import Artifact
|
|
7
9
|
from pulpcore.plugin.util import get_domain
|
|
8
10
|
from django.db.utils import IntegrityError
|
|
@@ -54,6 +56,27 @@ class PackageUploadSerializer(serializers.Serializer):
|
|
|
54
56
|
min_length=64,
|
|
55
57
|
max_length=64,
|
|
56
58
|
)
|
|
59
|
+
protocol_version = serializers.ChoiceField(
|
|
60
|
+
help_text=_("Protocol version to use for the upload. Only version 1 is supported."),
|
|
61
|
+
required=False,
|
|
62
|
+
choices=(1,),
|
|
63
|
+
default=1,
|
|
64
|
+
)
|
|
65
|
+
filetype = serializers.ChoiceField(
|
|
66
|
+
help_text=_("Type of artifact to upload."),
|
|
67
|
+
required=False,
|
|
68
|
+
choices=("bdist_wheel", "sdist"),
|
|
69
|
+
)
|
|
70
|
+
metadata_version = serializers.ChoiceField(
|
|
71
|
+
help_text=_("Metadata version of the uploaded package."),
|
|
72
|
+
required=False,
|
|
73
|
+
choices=SUPPORTED_METADATA_VERSIONS,
|
|
74
|
+
)
|
|
75
|
+
attestations = serializers.JSONField(
|
|
76
|
+
required=False,
|
|
77
|
+
help_text=_("A JSON list containing attestations for the package."),
|
|
78
|
+
write_only=True,
|
|
79
|
+
)
|
|
57
80
|
|
|
58
81
|
def validate(self, data):
|
|
59
82
|
"""Validates the request."""
|
|
@@ -63,14 +86,33 @@ class PackageUploadSerializer(serializers.Serializer):
|
|
|
63
86
|
file = data.get("content")
|
|
64
87
|
for ext, packagetype in DIST_EXTENSIONS.items():
|
|
65
88
|
if file.name.endswith(ext):
|
|
89
|
+
if (filetype := data.get("filetype")) and filetype != packagetype:
|
|
90
|
+
raise serializers.ValidationError(
|
|
91
|
+
{
|
|
92
|
+
"filetype": _(
|
|
93
|
+
"filetype {} does not match found filetype {} for file {}"
|
|
94
|
+
).format(filetype, packagetype, file.name)
|
|
95
|
+
}
|
|
96
|
+
)
|
|
66
97
|
break
|
|
67
98
|
else:
|
|
68
99
|
raise serializers.ValidationError(
|
|
69
|
-
|
|
70
|
-
"
|
|
71
|
-
|
|
72
|
-
|
|
100
|
+
{
|
|
101
|
+
"content": _(
|
|
102
|
+
"Extension on {} is not a valid python extension "
|
|
103
|
+
"(.whl, .exe, .egg, .tar.gz, .tar.bz2, .zip)"
|
|
104
|
+
).format(file.name)
|
|
105
|
+
}
|
|
73
106
|
)
|
|
107
|
+
|
|
108
|
+
if attestations := data.get("attestations"):
|
|
109
|
+
try:
|
|
110
|
+
attestations = TypeAdapter(list[Attestation]).validate_python(attestations)
|
|
111
|
+
except ValidationError as e:
|
|
112
|
+
raise serializers.ValidationError(
|
|
113
|
+
{"attestations": _("The uploaded attestations are not valid: {}".format(e))}
|
|
114
|
+
)
|
|
115
|
+
|
|
74
116
|
sha256 = data.get("sha256_digest")
|
|
75
117
|
digests = {"sha256": sha256} if sha256 else None
|
|
76
118
|
artifact = Artifact.init_and_validate(file, expected_digests=digests)
|