pulp-python 3.21.0__tar.gz → 3.22.1__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.21.0 → pulp_python-3.22.1}/CHANGES.md +50 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/MANIFEST.in +1 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/PKG-INFO +3 -2
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/__init__.py +1 -1
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/migrations/0017_pythonpackagecontent_size.py +1 -1
- pulp_python-3.22.1/pulp_python/app/migrations/0018_packageprovenance.py +59 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/models.py +46 -1
- pulp_python-3.22.1/pulp_python/app/provenance.py +71 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/pypi/serializers.py +47 -5
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/pypi/views.py +78 -25
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/serializers.py +141 -1
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/tasks/sync.py +41 -5
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/tasks/upload.py +60 -13
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/urls.py +12 -1
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/utils.py +46 -2
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/viewsets.py +33 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/pytest_plugin.py +30 -0
- pulp_python-3.22.1/pulp_python/tests/functional/api/test_attestations.py +242 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/tests/functional/api/test_pypi_apis.py +0 -30
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/tests/functional/api/test_pypi_simple_json_api.py +1 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/tests/functional/api/test_sync.py +12 -0
- pulp_python-3.22.1/pulp_python/tests/functional/api/test_upload.py +124 -0
- pulp_python-3.22.1/pulp_python/tests/functional/assets/shelf-reader-0.1.tar.gz.publish.attestation +1 -0
- pulp_python-3.22.1/pulp_python/tests/functional/assets/shelf_reader-0.1-py2-none-any.whl.publish.attestation +1 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/tests/functional/constants.py +8 -5
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python.egg-info/PKG-INFO +3 -2
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python.egg-info/SOURCES.txt +5 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python.egg-info/requires.txt +2 -1
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pyproject.toml +4 -3
- pulp_python-3.21.0/pulp_python/tests/functional/api/test_upload.py +0 -44
- {pulp_python-3.21.0 → pulp_python-3.22.1}/COMMITMENT +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/COPYRIGHT +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/LICENSE +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/README.md +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/functest_requirements.txt +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/__init__.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/global_access_conditions.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/management/__init__.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/management/commands/__init__.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/management/commands/repair-python-metadata.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/migrations/0001_initial.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/migrations/0001_squashed_0010_update_json_field.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/migrations/0002_pythonpackagecontent_python_version.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/migrations/0003_new_sync_filters.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/migrations/0004_DATA_swap_distribution_model.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/migrations/0005_pythonpackagecontent_sha256.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/migrations/0006_pythonrepository_autopublish.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/migrations/0007_pythonpackagecontent_mv-2-1.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/migrations/0008_pythonpackagecontent_unique_sha256.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/migrations/0009_pythondistribution_allow_uploads.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/migrations/0010_update_json_field.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/migrations/0011_alter_pythondistribution_distribution_ptr_and_more.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/migrations/0012_add_domain.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/migrations/0013_add_rbac_permissions.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/migrations/0014_pythonpackagecontent_dynamic_and_more.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/migrations/0015_alter_pythonpackagecontent_options.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/migrations/0016_pythonpackagecontent_metadata_sha256.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/migrations/__init__.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/modelresource.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/pypi/__init__.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/replica.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/settings.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/tasks/__init__.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/tasks/publish.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/tasks/repair.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/tasks/vulnerability_report.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/webserver_snippets/__init__.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/webserver_snippets/apache.conf +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/app/webserver_snippets/nginx.conf +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/tests/__init__.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/tests/functional/__init__.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/tests/functional/api/__init__.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/tests/functional/api/test_auto_publish.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/tests/functional/api/test_consume_content.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/tests/functional/api/test_crud_content_unit.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/tests/functional/api/test_crud_publications.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/tests/functional/api/test_crud_remotes.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/tests/functional/api/test_domains.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/tests/functional/api/test_download_content.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/tests/functional/api/test_export_import.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/tests/functional/api/test_full_mirror.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/tests/functional/api/test_rbac.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/tests/functional/api/test_repair.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/tests/functional/api/test_vulnerability_report.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/tests/functional/utils.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/tests/unit/__init__.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python/tests/unit/test_models.py +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python.egg-info/dependency_links.txt +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python.egg-info/entry_points.txt +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/pulp_python.egg-info/top_level.txt +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/setup.cfg +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/test_requirements.txt +0 -0
- {pulp_python-3.21.0 → pulp_python-3.22.1}/unittest_requirements.txt +0 -0
|
@@ -8,6 +8,29 @@
|
|
|
8
8
|
|
|
9
9
|
[//]: # (towncrier release notes start)
|
|
10
10
|
|
|
11
|
+
## 3.22.1 (2025-12-10) {: #3.22.1 }
|
|
12
|
+
|
|
13
|
+
#### Bugfixes {: #3.22.1-bugfix }
|
|
14
|
+
|
|
15
|
+
- Fixed edge-case migration error in 0017_pythonpackagecontent_size.
|
|
16
|
+
[#1042](https://github.com/pulp/pulp_python/issues/1042)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 3.22.0 (2025-12-09) {: #3.22.0 }
|
|
21
|
+
|
|
22
|
+
#### Features {: #3.22.0-feature }
|
|
23
|
+
|
|
24
|
+
- Added attestations field to package upload that will create a PEP 740 Provenance object for that content.
|
|
25
|
+
[#984](https://github.com/pulp/pulp_python/issues/984)
|
|
26
|
+
- Added the ability to upload PEP 740 Provenance files to repositories.
|
|
27
|
+
|
|
28
|
+
#### Bugfixes {: #3.22.0-bugfix }
|
|
29
|
+
|
|
30
|
+
- Added some sanity validation checks to twine upload endpoint.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
11
34
|
## 3.21.0 (2025-11-18) {: #3.21.0 }
|
|
12
35
|
|
|
13
36
|
#### Features {: #3.21.0-feature }
|
|
@@ -26,6 +49,15 @@
|
|
|
26
49
|
|
|
27
50
|
---
|
|
28
51
|
|
|
52
|
+
## 3.20.1 (2025-11-18) {: #3.20.1 }
|
|
53
|
+
|
|
54
|
+
#### Bugfixes {: #3.20.1-bugfix }
|
|
55
|
+
|
|
56
|
+
- Fixed pull-through caching not checking the repository if package was not present on remote.
|
|
57
|
+
[#1004](https://github.com/pulp/pulp_python/issues/1004)
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
29
61
|
## 3.20.0 (2025-11-07) {: #3.20.0 }
|
|
30
62
|
|
|
31
63
|
#### Features {: #3.20.0-feature }
|
|
@@ -214,6 +246,12 @@ No significant changes.
|
|
|
214
246
|
|
|
215
247
|
---
|
|
216
248
|
|
|
249
|
+
## 3.12.8 (2025-11-18) {: #3.12.8 }
|
|
250
|
+
|
|
251
|
+
No significant changes.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
217
255
|
## 3.12.7 (2025-07-23) {: #3.12.7 }
|
|
218
256
|
|
|
219
257
|
No significant changes.
|
|
@@ -300,6 +338,12 @@ No significant changes.
|
|
|
300
338
|
|
|
301
339
|
---
|
|
302
340
|
|
|
341
|
+
## 3.11.7 (2025-11-18) {: #3.11.7 }
|
|
342
|
+
|
|
343
|
+
No significant changes.
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
303
347
|
## 3.11.6 (2025-07-23) {: #3.11.6 }
|
|
304
348
|
|
|
305
349
|
No significant changes.
|
|
@@ -364,6 +408,12 @@ No significant changes.
|
|
|
364
408
|
|
|
365
409
|
---
|
|
366
410
|
|
|
411
|
+
## 3.10.2 (2025-11-18) {: #3.10.2 }
|
|
412
|
+
|
|
413
|
+
No significant changes
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
367
417
|
## 3.10.1 (2025-07-23) {: #3.10.1 }
|
|
368
418
|
|
|
369
419
|
### Bugfixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pulp-python
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.22.1
|
|
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
|
|
@@ -23,7 +23,8 @@ License-File: LICENSE
|
|
|
23
23
|
Requires-Dist: pulpcore<3.100,>=3.85.3
|
|
24
24
|
Requires-Dist: pkginfo<1.13.0,>=1.12.0
|
|
25
25
|
Requires-Dist: bandersnatch<6.7,>=6.6.0
|
|
26
|
-
Requires-Dist: pypi-simple<2.0,>=1.
|
|
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
|
|
@@ -14,7 +14,7 @@ def add_size_to_current_models(apps, schema_editor):
|
|
|
14
14
|
artifact = content_artifact.artifact
|
|
15
15
|
else:
|
|
16
16
|
artifact = RemoteArtifact.objects.filter(content_artifact=content_artifact).first()
|
|
17
|
-
python_package.size = artifact.size or 0
|
|
17
|
+
python_package.size = artifact and artifact.size or 0
|
|
18
18
|
package_bulk.append(python_package)
|
|
19
19
|
if len(package_bulk) == 100000:
|
|
20
20
|
with transaction.atomic():
|
|
@@ -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,
|
|
@@ -235,6 +242,43 @@ class PythonPackageContent(Content):
|
|
|
235
242
|
]
|
|
236
243
|
|
|
237
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
|
+
|
|
238
282
|
class PythonPublication(Publication, AutoAddObjPermsMixin):
|
|
239
283
|
"""
|
|
240
284
|
A Publication for PythonContent.
|
|
@@ -270,6 +314,7 @@ class PythonRemote(Remote, AutoAddObjPermsMixin):
|
|
|
270
314
|
exclude_platforms = ArrayField(
|
|
271
315
|
models.CharField(max_length=10, blank=True), choices=PLATFORMS, default=list
|
|
272
316
|
)
|
|
317
|
+
provenance = models.BooleanField(default=False)
|
|
273
318
|
|
|
274
319
|
def get_remote_artifact_url(self, relative_path=None, request=None):
|
|
275
320
|
"""Get url for remote_artifact"""
|
|
@@ -295,7 +340,7 @@ class PythonRepository(Repository, AutoAddObjPermsMixin):
|
|
|
295
340
|
"""
|
|
296
341
|
|
|
297
342
|
TYPE = "python"
|
|
298
|
-
CONTENT_TYPES = [PythonPackageContent]
|
|
343
|
+
CONTENT_TYPES = [PythonPackageContent, PackageProvenance]
|
|
299
344
|
REMOTE_TYPES = [PythonRemote]
|
|
300
345
|
PULL_THROUGH_SUPPORTED = True
|
|
301
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)
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import json
|
|
2
1
|
import logging
|
|
3
2
|
|
|
4
|
-
from aiohttp.client_exceptions import ClientError
|
|
5
3
|
from rest_framework.viewsets import ViewSet
|
|
6
4
|
from rest_framework.renderers import BrowsableAPIRenderer, JSONRenderer, TemplateHTMLRenderer
|
|
7
5
|
from rest_framework.response import Response
|
|
@@ -27,16 +25,15 @@ from itertools import chain
|
|
|
27
25
|
from packaging.utils import canonicalize_name
|
|
28
26
|
from urllib.parse import urljoin, urlparse, urlunsplit
|
|
29
27
|
from pathlib import PurePath
|
|
30
|
-
from pypi_simple import ACCEPT_JSON_PREFERRED, ProjectPage
|
|
31
28
|
|
|
32
29
|
from pulpcore.plugin.viewsets import OperationPostponedResponse
|
|
33
30
|
from pulpcore.plugin.tasking import dispatch
|
|
34
31
|
from pulpcore.plugin.util import get_domain, get_url
|
|
35
|
-
from pulpcore.plugin.exceptions import TimeoutException
|
|
36
32
|
from pulp_python.app.models import (
|
|
37
33
|
PythonDistribution,
|
|
38
34
|
PythonPackageContent,
|
|
39
35
|
PythonPublication,
|
|
36
|
+
PackageProvenance,
|
|
40
37
|
)
|
|
41
38
|
from pulp_python.app.pypi.serializers import (
|
|
42
39
|
SummarySerializer,
|
|
@@ -53,6 +50,7 @@ from pulp_python.app.utils import (
|
|
|
53
50
|
PYPI_LAST_SERIAL,
|
|
54
51
|
PYPI_SERIAL_CONSTANT,
|
|
55
52
|
get_remote_package_filter,
|
|
53
|
+
get_remote_simple_page,
|
|
56
54
|
)
|
|
57
55
|
|
|
58
56
|
from pulp_python.app import tasks
|
|
@@ -61,6 +59,7 @@ log = logging.getLogger(__name__)
|
|
|
61
59
|
|
|
62
60
|
ORIGIN_HOST = settings.CONTENT_ORIGIN if settings.CONTENT_ORIGIN else settings.PYPI_API_HOSTNAME
|
|
63
61
|
BASE_CONTENT_URL = urljoin(ORIGIN_HOST, settings.CONTENT_PATH_PREFIX)
|
|
62
|
+
BASE_API_URL = urljoin(settings.PYPI_API_HOSTNAME, "pypi/")
|
|
64
63
|
|
|
65
64
|
PYPI_SIMPLE_V1_HTML = "application/vnd.pypi.simple.v1+html"
|
|
66
65
|
PYPI_SIMPLE_V1_JSON = "application/vnd.pypi.simple.v1+json"
|
|
@@ -120,6 +119,11 @@ class PyPIMixin:
|
|
|
120
119
|
"""Returns queryset of the content in this repository version."""
|
|
121
120
|
return PythonPackageContent.objects.filter(pk__in=repository_version.content)
|
|
122
121
|
|
|
122
|
+
@staticmethod
|
|
123
|
+
def get_provenances(repository_version):
|
|
124
|
+
"""Returns queryset of the provenance for this repository version."""
|
|
125
|
+
return PackageProvenance.objects.filter(pk__in=repository_version.content)
|
|
126
|
+
|
|
123
127
|
def should_redirect(self, repo_version=None):
|
|
124
128
|
"""Checks if there is a publication the content app can serve."""
|
|
125
129
|
if self.distribution.publication:
|
|
@@ -139,10 +143,13 @@ class PyPIMixin:
|
|
|
139
143
|
def initial(self, request, *args, **kwargs):
|
|
140
144
|
"""Perform common initialization tasks for PyPI endpoints."""
|
|
141
145
|
super().initial(request, *args, **kwargs)
|
|
146
|
+
domain_name = get_domain().name
|
|
142
147
|
if settings.DOMAIN_ENABLED:
|
|
143
|
-
self.base_content_url = urljoin(BASE_CONTENT_URL, f"{
|
|
148
|
+
self.base_content_url = urljoin(BASE_CONTENT_URL, f"{domain_name}/")
|
|
149
|
+
self.base_api_url = urljoin(BASE_API_URL, f"{domain_name}/")
|
|
144
150
|
else:
|
|
145
151
|
self.base_content_url = BASE_CONTENT_URL
|
|
152
|
+
self.base_api_url = BASE_API_URL
|
|
146
153
|
|
|
147
154
|
@classmethod
|
|
148
155
|
def urlpattern(cls):
|
|
@@ -174,12 +181,15 @@ class PackageUploadMixin(PyPIMixin):
|
|
|
174
181
|
serializer = PackageUploadSerializer(data=request.data)
|
|
175
182
|
serializer.is_valid(raise_exception=True)
|
|
176
183
|
artifact, filename = serializer.validated_data["content"]
|
|
184
|
+
attestations = serializer.validated_data.get("attestations", None)
|
|
177
185
|
repo_content = self.get_content(self.get_repository_version(self.distribution))
|
|
178
186
|
if repo_content.filter(filename=filename).exists():
|
|
179
187
|
return HttpResponseBadRequest(reason=f"Package {filename} already exists in index")
|
|
180
188
|
|
|
181
189
|
if settings.PYTHON_GROUP_UPLOADS:
|
|
182
|
-
return self.upload_package_group(
|
|
190
|
+
return self.upload_package_group(
|
|
191
|
+
repo, artifact, filename, attestations, request.session
|
|
192
|
+
)
|
|
183
193
|
|
|
184
194
|
result = dispatch(
|
|
185
195
|
tasks.upload,
|
|
@@ -187,17 +197,20 @@ class PackageUploadMixin(PyPIMixin):
|
|
|
187
197
|
kwargs={
|
|
188
198
|
"artifact_sha256": artifact.sha256,
|
|
189
199
|
"filename": filename,
|
|
200
|
+
"attestations": attestations,
|
|
190
201
|
"repository_pk": str(repo.pk),
|
|
191
202
|
},
|
|
192
203
|
)
|
|
193
204
|
return OperationPostponedResponse(result, request)
|
|
194
205
|
|
|
195
|
-
def upload_package_group(self, repo, artifact, filename, session):
|
|
206
|
+
def upload_package_group(self, repo, artifact, filename, attestations, session):
|
|
196
207
|
"""Steps 4 & 5, spawns tasks to add packages to index."""
|
|
197
208
|
start_time = datetime.now(tz=timezone.utc) + timedelta(seconds=5)
|
|
198
209
|
task = "updated"
|
|
199
210
|
if not session.get("start"):
|
|
200
|
-
task = self.create_group_upload_task(
|
|
211
|
+
task = self.create_group_upload_task(
|
|
212
|
+
session, repo, artifact, filename, attestations, start_time
|
|
213
|
+
)
|
|
201
214
|
else:
|
|
202
215
|
sq = Session.objects.select_for_update(nowait=True).filter(pk=session.session_key)
|
|
203
216
|
try:
|
|
@@ -205,7 +218,7 @@ class PackageUploadMixin(PyPIMixin):
|
|
|
205
218
|
sq.first()
|
|
206
219
|
current_start = datetime.fromisoformat(session["start"])
|
|
207
220
|
if current_start >= datetime.now(tz=timezone.utc):
|
|
208
|
-
session["artifacts"].append((str(artifact.sha256), filename))
|
|
221
|
+
session["artifacts"].append((str(artifact.sha256), filename, attestations))
|
|
209
222
|
session["start"] = str(start_time)
|
|
210
223
|
session.modified = False
|
|
211
224
|
session.save()
|
|
@@ -213,14 +226,18 @@ class PackageUploadMixin(PyPIMixin):
|
|
|
213
226
|
raise DatabaseError
|
|
214
227
|
except DatabaseError:
|
|
215
228
|
session.cycle_key()
|
|
216
|
-
task = self.create_group_upload_task(
|
|
229
|
+
task = self.create_group_upload_task(
|
|
230
|
+
session, repo, artifact, filename, attestations, start_time
|
|
231
|
+
)
|
|
217
232
|
data = {"session": session.session_key, "task": task, "task_start_time": start_time}
|
|
218
233
|
return Response(data=data)
|
|
219
234
|
|
|
220
|
-
def create_group_upload_task(
|
|
235
|
+
def create_group_upload_task(
|
|
236
|
+
self, cur_session, repository, artifact, filename, attestations, start_time
|
|
237
|
+
):
|
|
221
238
|
"""Creates the actual task that adds the packages to the index."""
|
|
222
239
|
cur_session["start"] = str(start_time)
|
|
223
|
-
cur_session["artifacts"] = [(str(artifact.sha256), filename)]
|
|
240
|
+
cur_session["artifacts"] = [(str(artifact.sha256), filename, attestations)]
|
|
224
241
|
cur_session.modified = False
|
|
225
242
|
cur_session.save()
|
|
226
243
|
task = dispatch(
|
|
@@ -273,6 +290,13 @@ class SimpleView(PackageUploadMixin, ViewSet):
|
|
|
273
290
|
else:
|
|
274
291
|
return [JSONRenderer(), BrowsableAPIRenderer()]
|
|
275
292
|
|
|
293
|
+
def get_provenance_url(self, package, version, filename):
|
|
294
|
+
"""Gets the provenance url for a package."""
|
|
295
|
+
base_path = self.distribution.base_path
|
|
296
|
+
return urljoin(
|
|
297
|
+
self.base_api_url, f"{base_path}/integrity/{package}/{version}/{filename}/provenance/"
|
|
298
|
+
)
|
|
299
|
+
|
|
276
300
|
@extend_schema(summary="Get index simple page")
|
|
277
301
|
def list(self, request, path):
|
|
278
302
|
"""Gets the simple api html page for the index."""
|
|
@@ -308,26 +332,17 @@ class SimpleView(PackageUploadMixin, ViewSet):
|
|
|
308
332
|
"size": release_package.size,
|
|
309
333
|
"upload_time": release_package.upload_time,
|
|
310
334
|
"version": release_package.version,
|
|
335
|
+
"provenance": release_package.provenance_url,
|
|
311
336
|
}
|
|
312
337
|
|
|
313
338
|
rfilter = get_remote_package_filter(remote)
|
|
314
339
|
if not rfilter.filter_project(package):
|
|
315
340
|
return {}
|
|
316
341
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
remote.headers.append({"Accept": ACCEPT_JSON_PREFERRED})
|
|
320
|
-
downloader = remote.get_downloader(url=url, max_retries=1)
|
|
321
|
-
try:
|
|
322
|
-
d = downloader.fetch()
|
|
323
|
-
except (ClientError, TimeoutException):
|
|
342
|
+
page = get_remote_simple_page(package, remote)
|
|
343
|
+
if not page:
|
|
324
344
|
log.info(f"Failed to fetch {package} simple page from {remote.url}")
|
|
325
345
|
return {}
|
|
326
|
-
|
|
327
|
-
if d.headers["content-type"] == PYPI_SIMPLE_V1_JSON:
|
|
328
|
-
page = ProjectPage.from_json_data(json.load(open(d.path, "rb")), base_url=url)
|
|
329
|
-
else:
|
|
330
|
-
page = ProjectPage.from_html(package, open(d.path, "rb").read(), base_url=url)
|
|
331
346
|
return {
|
|
332
347
|
p.filename: parse_package(p)
|
|
333
348
|
for p in page.packages
|
|
@@ -348,7 +363,8 @@ class SimpleView(PackageUploadMixin, ViewSet):
|
|
|
348
363
|
elif self.should_redirect(repo_version=repo_ver):
|
|
349
364
|
return redirect(urljoin(self.base_content_url, f"{path}/simple/{normalized}/"))
|
|
350
365
|
if content:
|
|
351
|
-
|
|
366
|
+
local_packages = content.filter(name__normalize=normalized)
|
|
367
|
+
packages = local_packages.values(
|
|
352
368
|
"filename",
|
|
353
369
|
"sha256",
|
|
354
370
|
"metadata_sha256",
|
|
@@ -357,11 +373,19 @@ class SimpleView(PackageUploadMixin, ViewSet):
|
|
|
357
373
|
"pulp_created",
|
|
358
374
|
"version",
|
|
359
375
|
)
|
|
376
|
+
provenances = PackageProvenance.objects.filter(package__in=local_packages).values_list(
|
|
377
|
+
"package__filename", flat=True
|
|
378
|
+
)
|
|
360
379
|
local_releases = {
|
|
361
380
|
p["filename"]: {
|
|
362
381
|
**p,
|
|
363
382
|
"url": urljoin(self.base_content_url, f"{path}/{p['filename']}"),
|
|
364
383
|
"upload_time": p["pulp_created"],
|
|
384
|
+
"provenance": (
|
|
385
|
+
self.get_provenance_url(normalized, p["version"], p["filename"])
|
|
386
|
+
if p["filename"] in provenances
|
|
387
|
+
else None
|
|
388
|
+
),
|
|
365
389
|
}
|
|
366
390
|
for p in packages
|
|
367
391
|
}
|
|
@@ -497,3 +521,32 @@ class UploadView(PackageUploadMixin, ViewSet):
|
|
|
497
521
|
This is the endpoint that tools like Twine and Poetry use for their upload commands.
|
|
498
522
|
"""
|
|
499
523
|
return self.upload(request, path)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
class ProvenanceView(PyPIMixin, ViewSet):
|
|
527
|
+
"""View for the PyPI provenance endpoint."""
|
|
528
|
+
|
|
529
|
+
endpoint_name = "integrity"
|
|
530
|
+
DEFAULT_ACCESS_POLICY = {
|
|
531
|
+
"statements": [
|
|
532
|
+
{
|
|
533
|
+
"action": ["retrieve"],
|
|
534
|
+
"principal": "*",
|
|
535
|
+
"effect": "allow",
|
|
536
|
+
},
|
|
537
|
+
],
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
@extend_schema(summary="Get package provenance")
|
|
541
|
+
def retrieve(self, request, path, package, version, filename):
|
|
542
|
+
"""Gets the provenance for a package."""
|
|
543
|
+
repo_ver, content = self.get_rvc()
|
|
544
|
+
if content:
|
|
545
|
+
package_content = content.filter(
|
|
546
|
+
name__normalize=package, version=version, filename=filename
|
|
547
|
+
).first()
|
|
548
|
+
if package_content:
|
|
549
|
+
provenance = self.get_provenances(repo_ver).filter(package=package_content).first()
|
|
550
|
+
if provenance:
|
|
551
|
+
return Response(data=provenance.provenance)
|
|
552
|
+
return HttpResponseNotFound(f"{package} {version} {filename} provenance does not exist.")
|