Skip to content

Commit 362f6b7

Browse files
authored
feat(admin): add prohibited email domains (#16747)
* feat(admin): add prohibited email domains Signed-off-by: Mike Fiedler <[email protected]> * add permissions Signed-off-by: Mike Fiedler <[email protected]> * add views and templates Signed-off-by: Mike Fiedler <[email protected]> * feat: handle non-exact domain inputs Signed-off-by: Mike Fiedler <[email protected]> * refactor query to use `exists()` subquery Signed-off-by: Mike Fiedler <[email protected]> * fix: disallow using the live service during extraction If we have to do this a third time, we probably want to wrap the extractor in a utility function. Signed-off-by: Mike Fiedler <[email protected]> --------- Signed-off-by: Mike Fiedler <[email protected]>
1 parent f2150d1 commit 362f6b7

File tree

10 files changed

+594
-3
lines changed

10 files changed

+594
-3
lines changed

tests/common/db/accounts.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,21 @@
1313
import datetime
1414

1515
import factory
16+
import faker
1617

1718
from argon2 import PasswordHasher
1819

19-
from warehouse.accounts.models import Email, ProhibitedUserName, User
20+
from warehouse.accounts.models import (
21+
Email,
22+
ProhibitedEmailDomain,
23+
ProhibitedUserName,
24+
User,
25+
)
2026

2127
from .base import WarehouseFactory
2228

29+
fake = faker.Faker()
30+
2331

2432
class UserFactory(WarehouseFactory):
2533
class Meta:
@@ -90,6 +98,15 @@ class Meta:
9098
transient_bounces = 0
9199

92100

101+
class ProhibitedEmailDomainFactory(WarehouseFactory):
102+
class Meta:
103+
model = ProhibitedEmailDomain
104+
105+
# TODO: Replace when factory_boy supports `unique`.
106+
# See https://github.com/FactoryBoy/factory_boy/pull/997
107+
domain = factory.Sequence(lambda _: fake.unique.domain_name())
108+
109+
93110
class ProhibitedUsernameFactory(WarehouseFactory):
94111
class Meta:
95112
model = ProhibitedUserName

tests/unit/admin/test_routes.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,21 @@ def test_includeme():
284284
"/admin/prohibited_user_names/bulk/",
285285
domain=warehouse,
286286
),
287+
pretend.call(
288+
"admin.prohibited_email_domains.list",
289+
"/admin/prohibited_email_domains/",
290+
domain=warehouse,
291+
),
292+
pretend.call(
293+
"admin.prohibited_email_domains.add",
294+
"/admin/prohibited_email_domains/add/",
295+
domain=warehouse,
296+
),
297+
pretend.call(
298+
"admin.prohibited_email_domains.remove",
299+
"/admin/prohibited_email_domains/remove/",
300+
domain=warehouse,
301+
),
287302
pretend.call(
288303
"admin.observations.list",
289304
"/admin/observations/",
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
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 pyramid.httpexceptions import HTTPBadRequest, HTTPSeeOther
17+
18+
from warehouse.admin.views import prohibited_email_domains as views
19+
20+
from ....common.db.accounts import ProhibitedEmailDomain, ProhibitedEmailDomainFactory
21+
22+
23+
class TestProhibitedEmailDomainsList:
24+
def test_no_query(self, db_request):
25+
prohibited = sorted(
26+
ProhibitedEmailDomainFactory.create_batch(30),
27+
key=lambda b: b.created,
28+
)
29+
30+
result = views.prohibited_email_domains(db_request)
31+
32+
assert result == {"prohibited_email_domains": prohibited[:25], "query": None}
33+
34+
def test_with_page(self, db_request):
35+
prohibited = sorted(
36+
ProhibitedEmailDomainFactory.create_batch(30),
37+
key=lambda b: b.created,
38+
)
39+
db_request.GET["page"] = "2"
40+
41+
result = views.prohibited_email_domains(db_request)
42+
43+
assert result == {"prohibited_email_domains": prohibited[25:], "query": None}
44+
45+
def test_with_invalid_page(self):
46+
request = pretend.stub(params={"page": "not an integer"})
47+
48+
with pytest.raises(HTTPBadRequest):
49+
views.prohibited_email_domains(request)
50+
51+
def test_basic_query(self, db_request):
52+
prohibited = sorted(
53+
ProhibitedEmailDomainFactory.create_batch(30),
54+
key=lambda b: b.created,
55+
)
56+
db_request.GET["q"] = prohibited[0].domain
57+
58+
result = views.prohibited_email_domains(db_request)
59+
60+
assert result == {
61+
"prohibited_email_domains": [prohibited[0]],
62+
"query": prohibited[0].domain,
63+
}
64+
65+
def test_wildcard_query(self, db_request):
66+
prohibited = sorted(
67+
ProhibitedEmailDomainFactory.create_batch(30),
68+
key=lambda b: b.created,
69+
)
70+
db_request.GET["q"] = f"{prohibited[0].domain[:-1]}%"
71+
72+
result = views.prohibited_email_domains(db_request)
73+
74+
assert result == {
75+
"prohibited_email_domains": [prohibited[0]],
76+
"query": f"{prohibited[0].domain[:-1]}%",
77+
}
78+
79+
80+
class TestProhibitedEmailDomainsAdd:
81+
def test_no_email_domain(self, db_request):
82+
db_request.method = "POST"
83+
db_request.route_path = lambda a: "/admin/prohibited_email_domains/add/"
84+
db_request.session = pretend.stub(
85+
flash=pretend.call_recorder(lambda *a, **kw: None)
86+
)
87+
db_request.POST = {}
88+
89+
with pytest.raises(HTTPSeeOther):
90+
views.add_prohibited_email_domain(db_request)
91+
92+
assert db_request.session.flash.calls == [
93+
pretend.call("Email domain is required.", queue="error")
94+
]
95+
96+
def test_invalid_domain(self, db_request):
97+
db_request.method = "POST"
98+
db_request.route_path = lambda a: "/admin/prohibited_email_domains/add/"
99+
db_request.session = pretend.stub(
100+
flash=pretend.call_recorder(lambda *a, **kw: None)
101+
)
102+
db_request.POST = {"email_domain": "invalid"}
103+
104+
with pytest.raises(HTTPSeeOther):
105+
views.add_prohibited_email_domain(db_request)
106+
107+
assert db_request.session.flash.calls == [
108+
pretend.call("Invalid domain name 'invalid'", queue="error")
109+
]
110+
111+
def test_duplicate_domain(self, db_request):
112+
existing_domain = ProhibitedEmailDomainFactory.create()
113+
db_request.method = "POST"
114+
db_request.route_path = lambda a: "/admin/prohibited_email_domains/add/"
115+
db_request.session = pretend.stub(
116+
flash=pretend.call_recorder(lambda *a, **kw: None)
117+
)
118+
db_request.POST = {"email_domain": existing_domain.domain}
119+
120+
with pytest.raises(HTTPSeeOther):
121+
views.add_prohibited_email_domain(db_request)
122+
123+
assert db_request.session.flash.calls == [
124+
pretend.call(
125+
f"Email domain '{existing_domain.domain}' already exists.",
126+
queue="error",
127+
)
128+
]
129+
130+
@pytest.mark.parametrize(
131+
("input_domain", "expected_domain"),
132+
[
133+
("example.com", "example.com"),
134+
("mail.example.co.uk", "example.co.uk"),
135+
("https://example.com/", "example.com"),
136+
],
137+
)
138+
def test_success(self, db_request, input_domain, expected_domain):
139+
db_request.method = "POST"
140+
db_request.route_path = lambda a: "/admin/prohibited_email_domains/list/"
141+
db_request.session = pretend.stub(
142+
flash=pretend.call_recorder(lambda *a, **kw: None)
143+
)
144+
db_request.POST = {
145+
"email_domain": input_domain,
146+
"is_mx_record": "on",
147+
"comment": "testing",
148+
}
149+
150+
response = views.add_prohibited_email_domain(db_request)
151+
152+
assert response.status_code == 303
153+
assert response.headers["Location"] == "/admin/prohibited_email_domains/list/"
154+
assert db_request.session.flash.calls == [
155+
pretend.call("Prohibited email domain added.", queue="success")
156+
]
157+
158+
query = db_request.db.query(ProhibitedEmailDomain).filter(
159+
ProhibitedEmailDomain.domain == expected_domain
160+
)
161+
assert query.count() == 1
162+
assert query.one().is_mx_record
163+
assert query.one().comment == "testing"
164+
165+
166+
class TestProhibitedEmailDomainsRemove:
167+
def test_no_domain_name(self, db_request):
168+
db_request.method = "POST"
169+
db_request.route_path = lambda a: "/admin/prohibited_email_domains/remove/"
170+
db_request.session = pretend.stub(
171+
flash=pretend.call_recorder(lambda *a, **kw: None)
172+
)
173+
db_request.POST = {}
174+
175+
with pytest.raises(HTTPSeeOther):
176+
views.remove_prohibited_email_domain(db_request)
177+
178+
assert db_request.session.flash.calls == [
179+
pretend.call("Domain name is required.", queue="error")
180+
]
181+
182+
def test_domain_not_found(self, db_request):
183+
db_request.method = "POST"
184+
db_request.route_path = lambda a: "/admin/prohibited_email_domains/remove/"
185+
db_request.session = pretend.stub(
186+
flash=pretend.call_recorder(lambda *a, **kw: None)
187+
)
188+
db_request.POST = {"domain_name": "example.com"}
189+
190+
with pytest.raises(HTTPSeeOther):
191+
views.remove_prohibited_email_domain(db_request)
192+
193+
assert db_request.session.flash.calls == [
194+
pretend.call("Domain not found.", queue="error")
195+
]
196+
197+
def test_success(self, db_request):
198+
domain = ProhibitedEmailDomainFactory.create()
199+
db_request.method = "POST"
200+
db_request.route_path = lambda a: "/admin/prohibited_email_domains/list/"
201+
db_request.session = pretend.stub(
202+
flash=pretend.call_recorder(lambda *a, **kw: None)
203+
)
204+
db_request.POST = {"domain_name": domain.domain}
205+
206+
response = views.remove_prohibited_email_domain(db_request)
207+
208+
assert response.status_code == 303
209+
assert response.headers["Location"] == "/admin/prohibited_email_domains/list/"
210+
assert db_request.session.flash.calls == [
211+
pretend.call(
212+
f"Prohibited email domain '{domain.domain}' removed.", queue="success"
213+
)
214+
]
215+
216+
query = db_request.db.query(ProhibitedEmailDomain).filter(
217+
ProhibitedEmailDomain.domain == domain.domain
218+
)
219+
assert query.count() == 0

tests/unit/test_config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,8 @@ def test_root_factory_access_control_list():
574574
Permissions.AdminObservationsWrite,
575575
Permissions.AdminOrganizationsRead,
576576
Permissions.AdminOrganizationsWrite,
577+
Permissions.AdminProhibitedEmailDomainsRead,
578+
Permissions.AdminProhibitedEmailDomainsWrite,
577579
Permissions.AdminProhibitedProjectsRead,
578580
Permissions.AdminProhibitedProjectsWrite,
579581
Permissions.AdminProhibitedUsernameRead,
@@ -604,6 +606,7 @@ def test_root_factory_access_control_list():
604606
Permissions.AdminObservationsRead,
605607
Permissions.AdminObservationsWrite,
606608
Permissions.AdminOrganizationsRead,
609+
Permissions.AdminProhibitedEmailDomainsRead,
607610
Permissions.AdminProhibitedProjectsRead,
608611
Permissions.AdminProhibitedUsernameRead,
609612
Permissions.AdminProjectsRead,
@@ -629,6 +632,7 @@ def test_root_factory_access_control_list():
629632
Permissions.AdminObservationsRead,
630633
Permissions.AdminObservationsWrite,
631634
Permissions.AdminOrganizationsRead,
635+
Permissions.AdminProhibitedEmailDomainsRead,
632636
Permissions.AdminProhibitedProjectsRead,
633637
Permissions.AdminProhibitedUsernameRead,
634638
Permissions.AdminProjectsRead,

warehouse/admin/routes.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,22 @@ def includeme(config):
293293
"/admin/prohibited_user_names/bulk/",
294294
domain=warehouse,
295295
)
296+
# Prohibited Email related Admin pages
297+
config.add_route(
298+
"admin.prohibited_email_domains.list",
299+
"/admin/prohibited_email_domains/",
300+
domain=warehouse,
301+
)
302+
config.add_route(
303+
"admin.prohibited_email_domains.add",
304+
"/admin/prohibited_email_domains/add/",
305+
domain=warehouse,
306+
)
307+
config.add_route(
308+
"admin.prohibited_email_domains.remove",
309+
"/admin/prohibited_email_domains/remove/",
310+
domain=warehouse,
311+
)
296312

297313
# Observation related Admin pages
298314
config.add_route(

warehouse/admin/templates/admin/base.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,9 +175,9 @@
175175
</a>
176176
</li>
177177
<li class="nav-item">
178-
<a href="#" class="nav-link">
178+
<a href="{{ request.route_path('admin.prohibited_email_domains.list') }}" class="nav-link">
179179
<i class="nav-icon fa fa-envelope fa-flip-vertical"></i>
180-
<p>Email Domains <span class="right badge badge-warning">TODO</span></p>
180+
<p>Email Domains</p>
181181
</a>
182182
</li>
183183
</ul>

0 commit comments

Comments
 (0)