Skip to content

Commit ccff649

Browse files
authored
Move URL verification logic into its own file (#16592)
1 parent 1e0357d commit ccff649

File tree

4 files changed

+220
-183
lines changed

4 files changed

+220
-183
lines changed

tests/unit/forklift/test_legacy.py

-135
Original file line numberDiff line numberDiff line change
@@ -4880,138 +4880,3 @@ def test_missing_trailing_slash_redirect(pyramid_request):
48804880
"/legacy/ (with a trailing slash)"
48814881
)
48824882
assert resp.headers["Location"] == "/legacy/"
4883-
4884-
4885-
@pytest.mark.parametrize(
4886-
("url", "project_name", "project_normalized_name", "expected"),
4887-
[
4888-
( # PyPI /project/ case
4889-
"https://pypi.org/project/myproject",
4890-
"myproject",
4891-
"myproject",
4892-
True,
4893-
),
4894-
( # PyPI /p/ case
4895-
"https://pypi.org/p/myproject",
4896-
"myproject",
4897-
"myproject",
4898-
True,
4899-
),
4900-
( # pypi.python.org /project/ case
4901-
"https://pypi.python.org/project/myproject",
4902-
"myproject",
4903-
"myproject",
4904-
True,
4905-
),
4906-
( # pypi.python.org /p/ case
4907-
"https://pypi.python.org/p/myproject",
4908-
"myproject",
4909-
"myproject",
4910-
True,
4911-
),
4912-
( # python.org/pypi/ case
4913-
"https://python.org/pypi/myproject",
4914-
"myproject",
4915-
"myproject",
4916-
True,
4917-
),
4918-
( # Normalized name differs from URL
4919-
"https://pypi.org/project/my_project",
4920-
"my_project",
4921-
"my-project",
4922-
True,
4923-
),
4924-
( # Normalized name same as URL
4925-
"https://pypi.org/project/my-project",
4926-
"my_project",
4927-
"my-project",
4928-
True,
4929-
),
4930-
( # Trailing slash
4931-
"https://pypi.org/project/myproject/",
4932-
"myproject",
4933-
"myproject",
4934-
True,
4935-
),
4936-
( # Domains are case insensitive
4937-
"https://PyPI.org/project/myproject",
4938-
"myproject",
4939-
"myproject",
4940-
True,
4941-
),
4942-
( # Paths are case-sensitive
4943-
"https://pypi.org/Project/myproject",
4944-
"myproject",
4945-
"myproject",
4946-
False,
4947-
),
4948-
( # Wrong domain
4949-
"https://example.com/project/myproject",
4950-
"myproject",
4951-
"myproject",
4952-
False,
4953-
),
4954-
( # Wrong path
4955-
"https://pypi.org/something/myproject",
4956-
"myproject",
4957-
"myproject",
4958-
False,
4959-
),
4960-
( # Path has extra components
4961-
"https://pypi.org/something/myproject/something",
4962-
"myproject",
4963-
"myproject",
4964-
False,
4965-
),
4966-
( # Wrong package name
4967-
"https://pypi.org/project/otherproject",
4968-
"myproject",
4969-
"myproject",
4970-
False,
4971-
),
4972-
( # Similar package name
4973-
"https://pypi.org/project/myproject",
4974-
"myproject2",
4975-
"myproject2",
4976-
False,
4977-
),
4978-
( # Similar package name
4979-
"https://pypi.org/project/myproject2",
4980-
"myproject",
4981-
"myproject",
4982-
False,
4983-
),
4984-
],
4985-
)
4986-
def test_verify_url_pypi(url, project_name, project_normalized_name, expected):
4987-
assert (
4988-
legacy._verify_url_pypi(url, project_name, project_normalized_name) == expected
4989-
)
4990-
4991-
4992-
def test_verify_url():
4993-
# `_verify_url` is just a helper function that calls `_verify_url_pypi` and
4994-
# `OIDCPublisher.verify_url`, where the actual verification logic lives.
4995-
publisher_verifies = pretend.stub(verify_url=lambda url: True)
4996-
publisher_fails = pretend.stub(verify_url=lambda url: False)
4997-
4998-
assert legacy._verify_url(
4999-
url="https://pypi.org/project/myproject/",
5000-
publisher=None,
5001-
project_name="myproject",
5002-
project_normalized_name="myproject",
5003-
)
5004-
5005-
assert legacy._verify_url(
5006-
url="https://github.com/org/myproject/issues",
5007-
publisher=publisher_verifies,
5008-
project_name="myproject",
5009-
project_normalized_name="myproject",
5010-
)
5011-
5012-
assert not legacy._verify_url(
5013-
url="example.com",
5014-
publisher=publisher_fails,
5015-
project_name="myproject",
5016-
project_normalized_name="myproject",
5017-
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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 pretend
14+
import pytest
15+
16+
from warehouse.packaging.metadata_verification import _verify_url_pypi, verify_url
17+
18+
19+
@pytest.mark.parametrize(
20+
("url", "project_name", "project_normalized_name", "expected"),
21+
[
22+
( # PyPI /project/ case
23+
"https://pypi.org/project/myproject",
24+
"myproject",
25+
"myproject",
26+
True,
27+
),
28+
( # PyPI /p/ case
29+
"https://pypi.org/p/myproject",
30+
"myproject",
31+
"myproject",
32+
True,
33+
),
34+
( # pypi.python.org /project/ case
35+
"https://pypi.python.org/project/myproject",
36+
"myproject",
37+
"myproject",
38+
True,
39+
),
40+
( # pypi.python.org /p/ case
41+
"https://pypi.python.org/p/myproject",
42+
"myproject",
43+
"myproject",
44+
True,
45+
),
46+
( # python.org/pypi/ case
47+
"https://python.org/pypi/myproject",
48+
"myproject",
49+
"myproject",
50+
True,
51+
),
52+
( # Normalized name differs from URL
53+
"https://pypi.org/project/my_project",
54+
"my_project",
55+
"my-project",
56+
True,
57+
),
58+
( # Normalized name same as URL
59+
"https://pypi.org/project/my-project",
60+
"my_project",
61+
"my-project",
62+
True,
63+
),
64+
( # Trailing slash
65+
"https://pypi.org/project/myproject/",
66+
"myproject",
67+
"myproject",
68+
True,
69+
),
70+
( # Domains are case insensitive
71+
"https://PyPI.org/project/myproject",
72+
"myproject",
73+
"myproject",
74+
True,
75+
),
76+
( # Paths are case-sensitive
77+
"https://pypi.org/Project/myproject",
78+
"myproject",
79+
"myproject",
80+
False,
81+
),
82+
( # Wrong domain
83+
"https://example.com/project/myproject",
84+
"myproject",
85+
"myproject",
86+
False,
87+
),
88+
( # Wrong path
89+
"https://pypi.org/something/myproject",
90+
"myproject",
91+
"myproject",
92+
False,
93+
),
94+
( # Path has extra components
95+
"https://pypi.org/something/myproject/something",
96+
"myproject",
97+
"myproject",
98+
False,
99+
),
100+
( # Wrong package name
101+
"https://pypi.org/project/otherproject",
102+
"myproject",
103+
"myproject",
104+
False,
105+
),
106+
( # Similar package name
107+
"https://pypi.org/project/myproject",
108+
"myproject2",
109+
"myproject2",
110+
False,
111+
),
112+
( # Similar package name
113+
"https://pypi.org/project/myproject2",
114+
"myproject",
115+
"myproject",
116+
False,
117+
),
118+
],
119+
)
120+
def test_verify_url_pypi(url, project_name, project_normalized_name, expected):
121+
assert _verify_url_pypi(url, project_name, project_normalized_name) == expected
122+
123+
124+
def test_verify_url():
125+
# `verify_url` is just a helper function that calls `_verify_url_pypi` and
126+
# `OIDCPublisher.verify_url`, where the actual verification logic lives.
127+
publisher_verifies = pretend.stub(verify_url=lambda url: True)
128+
publisher_fails = pretend.stub(verify_url=lambda url: False)
129+
130+
assert verify_url(
131+
url="https://pypi.org/project/myproject/",
132+
publisher=None,
133+
project_name="myproject",
134+
project_normalized_name="myproject",
135+
)
136+
137+
assert verify_url(
138+
url="https://github.com/org/myproject/issues",
139+
publisher=publisher_verifies,
140+
project_name="myproject",
141+
project_normalized_name="myproject",
142+
)
143+
144+
assert not verify_url(
145+
url="example.com",
146+
publisher=publisher_fails,
147+
project_name="myproject",
148+
project_normalized_name="myproject",
149+
)

warehouse/forklift/legacy.py

+4-48
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import packaging.utils
2626
import packaging.version
2727
import packaging_legacy.version
28-
import rfc3986
2928
import sentry_sdk
3029
import wtforms
3130
import wtforms.validators
@@ -62,9 +61,9 @@
6261
from warehouse.forklift.forms import UploadForm, _filetype_extension_mapping
6362
from warehouse.macaroons.models import Macaroon
6463
from warehouse.metrics import IMetricsService
65-
from warehouse.oidc.models import OIDCPublisher
6664
from warehouse.oidc.views import is_from_reusable_workflow
6765
from warehouse.packaging.interfaces import IFileStorage, IProjectService
66+
from warehouse.packaging.metadata_verification import verify_url
6867
from warehouse.packaging.models import (
6968
Dependency,
7069
DependencyKind,
@@ -459,49 +458,6 @@ def _process_attestations(request, distribution: Distribution):
459458
metrics.increment("warehouse.upload.attestations.ok")
460459

461460

462-
_pypi_project_urls = [
463-
"https://pypi.org/project/",
464-
"https://pypi.org/p/",
465-
"https://pypi.python.org/project/",
466-
"https://pypi.python.org/p/",
467-
"https://python.org/pypi/",
468-
]
469-
470-
471-
def _verify_url_pypi(url: str, project_name: str, project_normalized_name: str) -> bool:
472-
candidate_urls = (
473-
f"{pypi_project_url}{name}{optional_slash}"
474-
for pypi_project_url in _pypi_project_urls
475-
for name in {project_name, project_normalized_name}
476-
for optional_slash in ["/", ""]
477-
)
478-
479-
user_uri = rfc3986.api.uri_reference(url).normalize()
480-
return any(
481-
user_uri == rfc3986.api.uri_reference(candidate_url).normalize()
482-
for candidate_url in candidate_urls
483-
)
484-
485-
486-
def _verify_url(
487-
url: str,
488-
publisher: OIDCPublisher | None,
489-
project_name: str,
490-
project_normalized_name: str,
491-
) -> bool:
492-
if _verify_url_pypi(
493-
url=url,
494-
project_name=project_name,
495-
project_normalized_name=project_normalized_name,
496-
):
497-
return True
498-
499-
if not publisher:
500-
return False
501-
502-
return publisher.verify_url(url)
503-
504-
505461
def _sort_releases(request: Request, project: Project):
506462
releases = (
507463
request.db.query(Release)
@@ -869,7 +825,7 @@ def file_upload(request):
869825
else {
870826
name: {
871827
"url": url,
872-
"verified": _verify_url(
828+
"verified": verify_url(
873829
url=url,
874830
publisher=request.oidc_publisher,
875831
project_name=project.name,
@@ -883,7 +839,7 @@ def file_upload(request):
883839
home_page_verified = (
884840
False
885841
if home_page is None
886-
else _verify_url(
842+
else verify_url(
887843
url=home_page,
888844
publisher=request.oidc_publisher,
889845
project_name=project.name,
@@ -895,7 +851,7 @@ def file_upload(request):
895851
download_url_verified = (
896852
False
897853
if download_url is None
898-
else _verify_url(
854+
else verify_url(
899855
url=download_url,
900856
publisher=request.oidc_publisher,
901857
project_name=project.name,

0 commit comments

Comments
 (0)