Skip to content

Commit 9233099

Browse files
authored
Don't allow users with unverified emails to upload at all (#4292)
1 parent 79574e4 commit 9233099

File tree

3 files changed

+72
-32
lines changed

3 files changed

+72
-32
lines changed

tests/unit/forklift/test_legacy.py

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,8 @@ def test_fails_invalid_version(self, pyramid_config, pyramid_request, version):
686686
pyramid_request.POST["protocol_version"] = version
687687
pyramid_request.flags = pretend.stub(enabled=lambda *a: False)
688688

689+
pyramid_request.user = pretend.stub(primary_email=pretend.stub(verified=True))
690+
689691
with pytest.raises(HTTPBadRequest) as excinfo:
690692
legacy.file_upload(pyramid_request)
691693

@@ -864,6 +866,9 @@ def test_fails_invalid_post_data(
864866
self, pyramid_config, db_request, post_data, message
865867
):
866868
pyramid_config.testing_securitypolicy(userid=1)
869+
user = UserFactory.create()
870+
EmailFactory.create(user=user)
871+
db_request.user = user
867872
db_request.POST = MultiDict(post_data)
868873

869874
with pytest.raises(HTTPBadRequest) as excinfo:
@@ -1045,6 +1050,9 @@ def test_fails_with_admin_flag_set(self, pyramid_config, db_request):
10451050
)
10461051
admin_flag.enabled = True
10471052
pyramid_config.testing_securitypolicy(userid=1)
1053+
user = UserFactory.create()
1054+
EmailFactory.create(user=user)
1055+
db_request.user = user
10481056
name = "fails-with-admin-flag"
10491057
db_request.POST = MultiDict(
10501058
{
@@ -1078,6 +1086,9 @@ def test_fails_with_admin_flag_set(self, pyramid_config, db_request):
10781086

10791087
def test_upload_fails_without_file(self, pyramid_config, db_request):
10801088
pyramid_config.testing_securitypolicy(userid=1)
1089+
user = UserFactory.create()
1090+
EmailFactory.create(user=user)
1091+
db_request.user = user
10811092
db_request.POST = MultiDict(
10821093
{
10831094
"metadata_version": "1.2",
@@ -1098,8 +1109,10 @@ def test_upload_fails_without_file(self, pyramid_config, db_request):
10981109

10991110
@pytest.mark.parametrize("value", [("UNKNOWN"), ("UNKNOWN\n\n")])
11001111
def test_upload_cleans_unknown_values(self, pyramid_config, db_request, value):
1101-
11021112
pyramid_config.testing_securitypolicy(userid=1)
1113+
user = UserFactory.create()
1114+
EmailFactory.create(user=user)
1115+
db_request.user = user
11031116
db_request.POST = MultiDict(
11041117
{
11051118
"metadata_version": "1.2",
@@ -1117,6 +1130,9 @@ def test_upload_cleans_unknown_values(self, pyramid_config, db_request, value):
11171130

11181131
def test_upload_escapes_nul_characters(self, pyramid_config, db_request):
11191132
pyramid_config.testing_securitypolicy(userid=1)
1133+
user = UserFactory.create()
1134+
EmailFactory.create(user=user)
1135+
db_request.user = user
11201136
db_request.POST = MultiDict(
11211137
{
11221138
"metadata_version": "1.2",
@@ -1321,6 +1337,7 @@ def test_upload_fails_invlaid_content_type(
13211337
pyramid_config.testing_securitypolicy(userid=1)
13221338
user = UserFactory.create()
13231339
EmailFactory.create(user=user)
1340+
db_request.user = user
13241341
project = ProjectFactory.create()
13251342
release = ReleaseFactory.create(project=project, version="1.0")
13261343
RoleFactory.create(user=user, project=project)
@@ -1359,6 +1376,7 @@ def test_upload_fails_with_legacy_type(self, pyramid_config, db_request):
13591376

13601377
user = UserFactory.create()
13611378
EmailFactory.create(user=user)
1379+
db_request.user = user
13621380
project = ProjectFactory.create()
13631381
release = ReleaseFactory.create(project=project, version="1.0")
13641382
RoleFactory.create(user=user, project=project)
@@ -1394,6 +1412,7 @@ def test_upload_fails_with_legacy_ext(self, pyramid_config, db_request):
13941412

13951413
user = UserFactory.create()
13961414
EmailFactory.create(user=user)
1415+
db_request.user = user
13971416
project = ProjectFactory.create()
13981417
release = ReleaseFactory.create(project=project, version="1.0")
13991418
RoleFactory.create(user=user, project=project)
@@ -1430,6 +1449,7 @@ def test_upload_fails_for_second_sdist(self, pyramid_config, db_request):
14301449
pyramid_config.testing_securitypolicy(userid=1)
14311450

14321451
user = UserFactory.create()
1452+
db_request.user = user
14331453
EmailFactory.create(user=user)
14341454
project = ProjectFactory.create()
14351455
release = ReleaseFactory.create(project=project, version="1.0")
@@ -1470,6 +1490,7 @@ def test_upload_fails_with_invalid_signature(self, pyramid_config, db_request, s
14701490
pyramid_config.testing_securitypolicy(userid=1)
14711491

14721492
user = UserFactory.create()
1493+
db_request.user = user
14731494
EmailFactory.create(user=user)
14741495
project = ProjectFactory.create()
14751496
release = ReleaseFactory.create(project=project, version="1.0")
@@ -1507,6 +1528,7 @@ def test_upload_fails_with_invalid_classifier(self, pyramid_config, db_request):
15071528
pyramid_config.testing_securitypolicy(userid=1)
15081529

15091530
user = UserFactory.create()
1531+
db_request.user = user
15101532
EmailFactory.create(user=user)
15111533
project = ProjectFactory.create()
15121534
release = ReleaseFactory.create(project=project, version="1.0")
@@ -1546,6 +1568,7 @@ def test_upload_fails_with_deprecated_classifier(self, pyramid_config, db_reques
15461568
pyramid_config.testing_securitypolicy(userid=1)
15471569

15481570
user = UserFactory.create()
1571+
db_request.user = user
15491572
EmailFactory.create(user=user)
15501573
project = ProjectFactory.create()
15511574
release = ReleaseFactory.create(project=project, version="1.0")
@@ -1622,6 +1645,7 @@ def test_upload_fails_with_invalid_digest(
16221645
pyramid_config.testing_securitypolicy(userid=1)
16231646

16241647
user = UserFactory.create()
1648+
db_request.user = user
16251649
EmailFactory.create(user=user)
16261650
project = ProjectFactory.create()
16271651
release = ReleaseFactory.create(project=project, version="1.0")
@@ -1659,6 +1683,7 @@ def test_upload_fails_with_invalid_file(self, pyramid_config, db_request):
16591683
pyramid_config.testing_securitypolicy(userid=1)
16601684

16611685
user = UserFactory.create()
1686+
db_request.user = user
16621687
EmailFactory.create(user=user)
16631688
project = ProjectFactory.create()
16641689
release = ReleaseFactory.create(project=project, version="1.0")
@@ -1691,6 +1716,7 @@ def test_upload_fails_with_too_large_file(self, pyramid_config, db_request):
16911716
pyramid_config.testing_securitypolicy(userid=1)
16921717

16931718
user = UserFactory.create()
1719+
db_request.user = user
16941720
EmailFactory.create(user=user)
16951721
project = ProjectFactory.create(
16961722
name="foobar", upload_limit=(60 * 1024 * 1024) # 60 MB
@@ -1732,6 +1758,7 @@ def test_upload_fails_with_too_large_signature(self, pyramid_config, db_request)
17321758
pyramid_config.testing_securitypolicy(userid=1)
17331759

17341760
user = UserFactory.create()
1761+
db_request.user = user
17351762
EmailFactory.create(user=user)
17361763
project = ProjectFactory.create()
17371764
release = ReleaseFactory.create(project=project, version="1.0")
@@ -1770,6 +1797,7 @@ def test_upload_fails_with_previously_used_filename(
17701797
pyramid_config.testing_securitypolicy(userid=1)
17711798

17721799
user = UserFactory.create()
1800+
db_request.user = user
17731801
EmailFactory.create(user=user)
17741802
project = ProjectFactory.create()
17751803
release = ReleaseFactory.create(project=project, version="1.0")
@@ -1813,6 +1841,7 @@ def test_upload_noop_with_existing_filename_same_content(
18131841
pyramid_config.testing_securitypolicy(userid=1)
18141842

18151843
user = UserFactory.create()
1844+
db_request.user = user
18161845
EmailFactory.create(user=user)
18171846
project = ProjectFactory.create()
18181847
release = ReleaseFactory.create(project=project, version="1.0")
@@ -1859,6 +1888,7 @@ def test_upload_fails_with_existing_filename_diff_content(
18591888
pyramid_config.testing_securitypolicy(userid=1)
18601889

18611890
user = UserFactory.create()
1891+
db_request.user = user
18621892
EmailFactory.create(user=user)
18631893
project = ProjectFactory.create()
18641894
release = ReleaseFactory.create(project=project, version="1.0")
@@ -1910,6 +1940,8 @@ def test_upload_fails_with_diff_filename_same_blake2(
19101940
pyramid_config.testing_securitypolicy(userid=1)
19111941

19121942
user = UserFactory.create()
1943+
db_request.user = user
1944+
EmailFactory.create(user=user)
19131945
project = ProjectFactory.create()
19141946
release = ReleaseFactory.create(project=project, version="1.0")
19151947
RoleFactory.create(user=user, project=project)
@@ -1961,6 +1993,7 @@ def test_upload_fails_with_wrong_filename(self, pyramid_config, db_request):
19611993
pyramid_config.testing_securitypolicy(userid=1)
19621994

19631995
user = UserFactory.create()
1996+
db_request.user = user
19641997
EmailFactory.create(user=user)
19651998
project = ProjectFactory.create()
19661999
release = ReleaseFactory.create(project=project, version="1.0")
@@ -1999,6 +2032,7 @@ def test_upload_fails_with_invalid_extension(self, pyramid_config, db_request):
19992032
pyramid_config.testing_securitypolicy(userid=1)
20002033

20012034
user = UserFactory.create()
2035+
db_request.user = user
20022036
EmailFactory.create(user=user)
20032037
project = ProjectFactory.create()
20042038
release = ReleaseFactory.create(project=project, version="1.0")
@@ -2039,6 +2073,7 @@ def test_upload_fails_with_unsafe_filename(
20392073
pyramid_config.testing_securitypolicy(userid=1)
20402074

20412075
user = UserFactory.create()
2076+
db_request.user = user
20422077
EmailFactory.create(user=user)
20432078
project = ProjectFactory.create()
20442079
release = ReleaseFactory.create(project=project, version="1.0")
@@ -2430,6 +2465,7 @@ def test_upload_fails_with_unsupported_wheel_plat(
24302465
pyramid_config.testing_securitypolicy(userid=1)
24312466

24322467
user = UserFactory.create()
2468+
db_request.user = user
24332469
EmailFactory.create(user=user)
24342470
project = ProjectFactory.create()
24352471
release = ReleaseFactory.create(project=project, version="1.0")
@@ -2729,11 +2765,13 @@ def test_upload_succeeds_creates_project(self, pyramid_config, db_request):
27292765
@pytest.mark.parametrize(
27302766
("emails_verified", "expected_success"),
27312767
[
2732-
((True,), True),
2733-
((False,), False),
2734-
((True, True), True),
2735-
((True, False), True),
2736-
((False, False), False),
2768+
([], False),
2769+
([True], True),
2770+
([False], False),
2771+
([True, True], True),
2772+
([True, False], True),
2773+
([False, False], False),
2774+
([False, True], False),
27372775
],
27382776
)
27392777
def test_upload_requires_verified_email(
@@ -2742,8 +2780,8 @@ def test_upload_requires_verified_email(
27422780
pyramid_config.testing_securitypolicy(userid=1)
27432781

27442782
user = UserFactory.create()
2745-
for verified in emails_verified:
2746-
EmailFactory.create(user=user, verified=verified)
2783+
for i, verified in enumerate(emails_verified):
2784+
EmailFactory.create(user=user, verified=verified, primary=i == 0)
27472785

27482786
filename = "{}-{}.tar.gz".format("example", "1.0")
27492787

@@ -2782,11 +2820,10 @@ def test_upload_requires_verified_email(
27822820
assert resp.status_code == 400
27832821
assert resp.status == (
27842822
(
2785-
"400 User {!r} has no verified email "
2786-
"addresses, verify at least one "
2787-
"address before registering a new project "
2788-
"on PyPI. See /the/help/url/ "
2789-
"for more information."
2823+
"400 User {!r} does not have a verified primary email "
2824+
"address. Please add a verified primary email before "
2825+
"attempting to upload to PyPI. See /the/help/url/ for "
2826+
"more information.for more information."
27902827
).format(user.username)
27912828
)
27922829

warehouse/forklift/legacy.py

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -701,6 +701,26 @@ def file_upload(request):
701701
HTTPForbidden, "Invalid or non-existent authentication information."
702702
)
703703

704+
# Ensure that user has a verified, primary email address. This should both
705+
# reduce the ease of spam account creation and activty, as well as act as
706+
# a forcing function for https://github.com/pypa/warehouse/issues/3632.
707+
# TODO: Once https://github.com/pypa/warehouse/issues/3632 has been solved,
708+
# we might consider a different condition, possibly looking at
709+
# User.is_active instead.
710+
if not (request.user.primary_email and request.user.primary_email.verified):
711+
raise _exc_with_message(
712+
HTTPBadRequest,
713+
(
714+
"User {!r} does not have a verified primary email address. "
715+
"Please add a verified primary email before attempting to "
716+
"upload to PyPI. See {project_help} for more information."
717+
"for more information."
718+
).format(
719+
request.user.username,
720+
project_help=request.help_url(_anchor="verified-email"),
721+
),
722+
) from None
723+
704724
# Do some cleanup of the various form fields
705725
for key in list(request.POST):
706726
value = request.POST.get(key)
@@ -796,24 +816,6 @@ def file_upload(request):
796816
).format(projecthelp=request.help_url(_anchor="admin-intervention")),
797817
) from None
798818

799-
# Ensure that user has at least one verified email address. This should
800-
# reduce the ease of spam account creation and activity.
801-
# TODO: Once legacy is shutdown consider the condition here, perhaps
802-
# move to user.is_active or some other boolean
803-
if not any(email.verified for email in request.user.emails):
804-
raise _exc_with_message(
805-
HTTPBadRequest,
806-
(
807-
"User {!r} has no verified email addresses, "
808-
"verify at least one address before registering "
809-
"a new project on PyPI. See {projecthelp} "
810-
"for more information."
811-
).format(
812-
request.user.username,
813-
projecthelp=request.help_url(_anchor="verified-email"),
814-
),
815-
) from None
816-
817819
# Before we create the project, we're going to check our blacklist to
818820
# see if this project is even allowed to be registered. If it is not,
819821
# then we're going to deny the request to create this project.

warehouse/templates/pages/help.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ <h3 id="verified-email">{{ verified_email() }}</h3>
146146
Currently, PyPI requires a verified email address to perform the following operations:
147147
</p>
148148
<ul>
149-
<li>Register a new project</li>
149+
<li>Register a new project.</li>
150+
<li>Upload a new version or file.</li>
150151
</ul>
151152
<p>
152153
The list of activities that require a verified email address is likely to grow over time.

0 commit comments

Comments
 (0)