Skip to content

Commit 88c8488

Browse files
committed
Add more fields and validation to twine upload endpoint
1 parent 12e605a commit 88c8488

File tree

5 files changed

+178
-35
lines changed

5 files changed

+178
-35
lines changed

pulp_python/app/pypi/serializers.py

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
from gettext import gettext as _
33

44
from rest_framework import serializers
5-
from pulp_python.app.utils import DIST_EXTENSIONS
5+
from pulp_python.app.utils import DIST_EXTENSIONS, SUPPORTED_METADATA_VERSIONS
66
from pulpcore.plugin.models import Artifact
77
from pulpcore.plugin.util import get_domain
88
from django.db.utils import IntegrityError
9+
from packaging.version import Version, InvalidVersion
910

1011
log = logging.getLogger(__name__)
1112

@@ -54,6 +55,34 @@ class PackageUploadSerializer(serializers.Serializer):
5455
min_length=64,
5556
max_length=64,
5657
)
58+
protocol_version = serializers.IntegerField(
59+
help_text=_("Protocol version to use for the upload. Only version 1 is supported."),
60+
required=False,
61+
default=1,
62+
min_value=1,
63+
max_value=1,
64+
)
65+
filetype = serializers.ChoiceField(
66+
help_text=_("Type of artifact to upload."),
67+
required=False,
68+
choices=("bdist_wheel", "sdist"),
69+
)
70+
pyversion = serializers.CharField(
71+
help_text=_("Python tag for bdist_wheel uploads, source for sdist uploads."),
72+
required=False,
73+
)
74+
metadata_version = serializers.CharField(
75+
help_text=_("Metadata version of the uploaded package."),
76+
required=False,
77+
)
78+
name = serializers.CharField(
79+
help_text=_("Name of the package."),
80+
required=False,
81+
)
82+
version = serializers.CharField(
83+
help_text=_("Version of the package."),
84+
required=False,
85+
)
5786

5887
def validate(self, data):
5988
"""Validates the request."""
@@ -63,14 +92,45 @@ def validate(self, data):
6392
file = data.get("content")
6493
for ext, packagetype in DIST_EXTENSIONS.items():
6594
if file.name.endswith(ext):
95+
if filetype := data.get("filetype"):
96+
if filetype != packagetype:
97+
raise serializers.ValidationError(
98+
{
99+
"filetype": _(
100+
"filetype {} does not match found filetype {} for file {}"
101+
).format(filetype, packagetype, file.name)
102+
}
103+
)
66104
break
67105
else:
68106
raise serializers.ValidationError(
69-
_(
70-
"Extension on {} is not a valid python extension "
71-
"(.whl, .exe, .egg, .tar.gz, .tar.bz2, .zip)"
72-
).format(file.name)
107+
{
108+
"content": _(
109+
"Extension on {} is not a valid python extension "
110+
"(.whl, .exe, .egg, .tar.gz, .tar.bz2, .zip)"
111+
).format(file.name)
112+
}
73113
)
114+
if metadata_version := data.get("metadata_version"):
115+
try:
116+
md_version = Version(metadata_version)
117+
except InvalidVersion:
118+
raise serializers.ValidationError(
119+
{
120+
"metadata_version": _("metadata_version {} is not a valid version").format(
121+
metadata_version
122+
)
123+
}
124+
)
125+
else:
126+
if md_version not in SUPPORTED_METADATA_VERSIONS:
127+
raise serializers.ValidationError(
128+
{
129+
"metadata_version": _("metadata_version {} is not supported").format(
130+
metadata_version
131+
)
132+
}
133+
)
74134
sha256 = data.get("sha256_digest")
75135
digests = {"sha256": sha256} if sha256 else None
76136
artifact = Artifact.init_and_validate(file, expected_digests=digests)

pulp_python/app/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
PYPI_LAST_SERIAL = "X-PYPI-LAST-SERIAL"
1616
"""TODO This serial constant is temporary until Python repositories implements serials"""
1717
PYPI_SERIAL_CONSTANT = 1000000000
18+
SUPPORTED_METADATA_VERSIONS = {parse(v) for v in ("1.0", "1.1", "1.2", "2.0", "2.1", "2.2", "2.3", "2.4")} # noqa: E501
1819

1920
simple_index_template = """<!DOCTYPE html>
2021
<html>

pulp_python/pytest_plugin.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
PYTHON_XS_PROJECT_SPECIFIER,
99
PYTHON_EGG_FILENAME,
1010
PYTHON_URL,
11+
PYTHON_EGG_URL,
12+
PYTHON_WHEEL_URL,
13+
PYTHON_WHEEL_FILENAME,
1114
)
1215

1316

@@ -183,6 +186,20 @@ def _gen_python_content(relative_path=PYTHON_EGG_FILENAME, url=None, **body):
183186
yield _gen_python_content
184187

185188

189+
@pytest.fixture
190+
def python_empty_repo_distro(python_repo_factory, python_distribution_factory):
191+
"""Returns an empty repo with and distribution serving it."""
192+
193+
def _generate_empty_repo_distro(repo_body=None, distro_body=None):
194+
repo_body = repo_body or {}
195+
distro_body = distro_body or {}
196+
repo = python_repo_factory(**repo_body)
197+
distro = python_distribution_factory(repository=repo, **distro_body)
198+
return repo, distro
199+
200+
yield _generate_empty_repo_distro
201+
202+
186203
# Utility fixtures
187204

188205

@@ -217,3 +234,16 @@ def _gen_summary(repository_version=None, repository=None, version=None):
217234
def get_href(item):
218235
"""Tries to get the href from the given item, whether it is a string or object."""
219236
return item if isinstance(item, str) else item.pulp_href
237+
238+
239+
@pytest.fixture(scope="session")
240+
def python_package_dist_directory(tmp_path_factory, http_get):
241+
"""Creates a temp dir to hold package distros for uploading."""
242+
dist_dir = tmp_path_factory.mktemp("dist")
243+
egg_file = dist_dir / PYTHON_EGG_FILENAME
244+
wheel_file = dist_dir / PYTHON_WHEEL_FILENAME
245+
with open(egg_file, "wb") as f:
246+
f.write(http_get(PYTHON_EGG_URL))
247+
with open(wheel_file, "wb") as f:
248+
f.write(http_get(PYTHON_WHEEL_URL))
249+
yield dist_dir, egg_file, wheel_file

pulp_python/tests/functional/api/test_pypi_apis.py

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@
1111
PYTHON_MD_PROJECT_SPECIFIER,
1212
PYTHON_MD_PYPI_SUMMARY,
1313
PYTHON_EGG_FILENAME,
14-
PYTHON_EGG_URL,
1514
PYTHON_EGG_SHA256,
16-
PYTHON_WHEEL_FILENAME,
17-
PYTHON_WHEEL_URL,
1815
PYTHON_WHEEL_SHA256,
1916
SHELF_PYTHON_JSON,
2017
)
@@ -26,20 +23,6 @@
2623
PYPI_SERIAL_CONSTANT = 1000000000
2724

2825

29-
@pytest.fixture
30-
def python_empty_repo_distro(python_repo_factory, python_distribution_factory):
31-
"""Returns an empty repo with and distribution serving it."""
32-
33-
def _generate_empty_repo_distro(repo_body=None, distro_body=None):
34-
repo_body = repo_body or {}
35-
distro_body = distro_body or {}
36-
repo = python_repo_factory(**repo_body)
37-
distro = python_distribution_factory(repository=repo, **distro_body)
38-
return repo, distro
39-
40-
yield _generate_empty_repo_distro
41-
42-
4326
@pytest.mark.parallel
4427
def test_empty_index(python_bindings, python_empty_repo_distro):
4528
"""Checks that summary stats are 0 when index is empty."""
@@ -80,19 +63,6 @@ def test_published_index(
8063
assert summary.to_dict() == PYTHON_MD_PYPI_SUMMARY
8164

8265

83-
@pytest.fixture(scope="module")
84-
def python_package_dist_directory(tmp_path_factory, http_get):
85-
"""Creates a temp dir to hold package distros for uploading."""
86-
dist_dir = tmp_path_factory.mktemp("dist")
87-
egg_file = dist_dir / PYTHON_EGG_FILENAME
88-
wheel_file = dist_dir / PYTHON_WHEEL_FILENAME
89-
with open(egg_file, "wb") as f:
90-
f.write(http_get(PYTHON_EGG_URL))
91-
with open(wheel_file, "wb") as f:
92-
f.write(http_get(PYTHON_WHEEL_URL))
93-
yield dist_dir, egg_file, wheel_file
94-
95-
9666
@pytest.mark.parallel
9767
def test_package_upload(
9868
python_content_summary, python_empty_repo_distro, python_package_dist_directory, monitor_task

pulp_python/tests/functional/api/test_upload.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import pytest
2+
import requests
23
from pulp_python.tests.functional.constants import (
34
PYTHON_EGG_FILENAME,
45
PYTHON_EGG_URL,
56
PYTHON_WHEEL_FILENAME,
67
PYTHON_WHEEL_URL,
8+
PYTHON_EGG_SHA256,
9+
PYTHON_WHEEL_SHA256,
710
)
11+
from urllib.parse import urljoin
812

913

1014
@pytest.mark.parametrize(
@@ -42,3 +46,81 @@ def test_synchronous_package_upload(
4246
with pytest.raises(python_bindings.ApiException) as ctx:
4347
python_bindings.ContentPackagesApi.upload(**content_body)
4448
assert ctx.value.status == 403
49+
50+
51+
@pytest.mark.parallel
52+
def test_legacy_upload_invalid_protocol_version(
53+
python_empty_repo_distro, python_package_dist_directory
54+
):
55+
_, egg_file, _ = python_package_dist_directory
56+
_, distro = python_empty_repo_distro()
57+
url = urljoin(distro.base_url, "legacy/")
58+
with open(egg_file, "rb") as f:
59+
response = requests.post(
60+
url,
61+
data={"sha256_digest": PYTHON_EGG_SHA256, "protocol_version": 2},
62+
files={"content": f},
63+
auth=("admin", "password"),
64+
)
65+
assert response.status_code == 400
66+
assert response.json()["protocol_version"] == ["Ensure this value is less than or equal to 1."]
67+
68+
with open(egg_file, "rb") as f:
69+
response = requests.post(
70+
url,
71+
data={"sha256_digest": PYTHON_EGG_SHA256, "protocol_version": 0},
72+
files={"content": f},
73+
auth=("admin", "password"),
74+
)
75+
assert response.status_code == 400
76+
assert response.json()["protocol_version"] == [
77+
"Ensure this value is greater than or equal to 1."
78+
]
79+
80+
81+
@pytest.mark.parallel
82+
def test_legacy_upload_invalid_filetype(python_empty_repo_distro, python_package_dist_directory):
83+
_, egg_file, wheel_file = python_package_dist_directory
84+
_, distro = python_empty_repo_distro()
85+
url = urljoin(distro.base_url, "legacy/")
86+
with open(egg_file, "rb") as f:
87+
response = requests.post(
88+
url,
89+
data={"sha256_digest": PYTHON_EGG_SHA256, "filetype": "bdist_wheel"},
90+
files={"content": f},
91+
auth=("admin", "password"),
92+
)
93+
assert response.status_code == 400
94+
assert response.json()["filetype"] == [
95+
"filetype bdist_wheel does not match found filetype sdist for file shelf-reader-0.1.tar.gz"
96+
]
97+
98+
with open(wheel_file, "rb") as f:
99+
response = requests.post(
100+
url,
101+
data={"sha256_digest": PYTHON_WHEEL_SHA256, "filetype": "sdist"},
102+
files={"content": f},
103+
auth=("admin", "password"),
104+
)
105+
assert response.status_code == 400
106+
assert response.json()["filetype"] == [
107+
"filetype sdist does not match found filetype bdist_wheel for file shelf_reader-0.1-py2-none-any.whl" # noqa: E501
108+
]
109+
110+
111+
@pytest.mark.parallel
112+
def test_legacy_upload_invalid_metadata_version(
113+
python_empty_repo_distro, python_package_dist_directory
114+
):
115+
_, egg_file, _ = python_package_dist_directory
116+
_, distro = python_empty_repo_distro()
117+
url = urljoin(distro.base_url, "legacy/")
118+
with open(egg_file, "rb") as f:
119+
response = requests.post(
120+
url,
121+
data={"sha256_digest": PYTHON_EGG_SHA256, "metadata_version": "3.0"},
122+
files={"content": f},
123+
auth=("admin", "password"),
124+
)
125+
assert response.status_code == 400
126+
assert response.json()["metadata_version"] == ["metadata_version 3.0 is not supported"]

0 commit comments

Comments
 (0)