Skip to content

Commit

Permalink
Add email domain blocklist (#15672)
Browse files Browse the repository at this point in the history
  • Loading branch information
di committed Mar 28, 2024
1 parent 447aef7 commit 7d255a3
Show file tree
Hide file tree
Showing 14 changed files with 439 additions and 122 deletions.
55 changes: 51 additions & 4 deletions tests/unit/accounts/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
NoRecoveryCodes,
TooManyFailedLogins,
)
from warehouse.accounts.models import DisableReason
from warehouse.accounts.models import DisableReason, ProhibitedEmailDomain
from warehouse.captcha import recaptcha
from warehouse.events.tags import EventTag
from warehouse.utils.webauthn import AuthenticationRejectedError
Expand Down Expand Up @@ -399,6 +399,9 @@ def test_validate(self):
)

form = forms.RegistrationForm(
request=pretend.stub(
db=pretend.stub(query=lambda *a: pretend.stub(scalar=lambda: False))
),
formdata=MultiDict(
{
"username": "myusername",
Expand All @@ -419,6 +422,7 @@ def test_validate(self):

def test_password_confirm_required_error(self):
form = forms.RegistrationForm(
request=pretend.stub(),
formdata=MultiDict({"password_confirm": ""}),
user_service=pretend.stub(
find_userid_by_email=pretend.call_recorder(lambda _: pretend.stub())
Expand All @@ -435,6 +439,7 @@ def test_passwords_mismatch_error(self, pyramid_config):
find_userid_by_email=pretend.call_recorder(lambda _: pretend.stub())
)
form = forms.RegistrationForm(
request=pretend.stub(),
formdata=MultiDict(
{"new_password": "password", "password_confirm": "mismatch"}
),
Expand All @@ -454,6 +459,7 @@ def test_passwords_match_success(self):
find_userid_by_email=pretend.call_recorder(lambda _: pretend.stub())
)
form = forms.RegistrationForm(
request=pretend.stub(),
formdata=MultiDict(
{
"new_password": "MyStr0ng!shPassword",
Expand All @@ -471,6 +477,7 @@ def test_passwords_match_success(self):

def test_email_required_error(self):
form = forms.RegistrationForm(
request=pretend.stub(),
formdata=MultiDict({"email": ""}),
user_service=pretend.stub(
find_userid_by_email=pretend.call_recorder(lambda _: pretend.stub())
Expand All @@ -483,8 +490,9 @@ def test_email_required_error(self):
assert form.email.errors.pop() == "This field is required."

@pytest.mark.parametrize("email", ["bad", "foo]bar@example.com", "</body></html>"])
def test_invalid_email_error(self, pyramid_config, email):
def test_invalid_email_error(self, pyramid_request, email):
form = forms.RegistrationForm(
request=pyramid_request,
formdata=MultiDict({"email": email}),
user_service=pretend.stub(
find_userid_by_email=pretend.call_recorder(lambda _: None)
Expand All @@ -500,6 +508,9 @@ def test_invalid_email_error(self, pyramid_config, email):

def test_exotic_email_success(self):
form = forms.RegistrationForm(
request=pretend.stub(
db=pretend.stub(query=lambda *a: pretend.stub(scalar=lambda: False))
),
formdata=MultiDict({"email": "foo@n--tree.net"}),
user_service=pretend.stub(
find_userid_by_email=pretend.call_recorder(lambda _: None)
Expand All @@ -511,8 +522,12 @@ def test_exotic_email_success(self):
form.validate()
assert len(form.email.errors) == 0

def test_email_exists_error(self, pyramid_config):
def test_email_exists_error(self, pyramid_request):
pyramid_request.db = pretend.stub(
query=lambda *a: pretend.stub(scalar=lambda: False)
)
form = forms.RegistrationForm(
request=pyramid_request,
formdata=MultiDict({"email": "foo@bar.com"}),
user_service=pretend.stub(
find_userid_by_email=pretend.call_recorder(lambda _: pretend.stub())
Expand All @@ -528,8 +543,9 @@ def test_email_exists_error(self, pyramid_config):
"Use a different email."
)

def test_prohibited_email_error(self, pyramid_config):
def test_disposable_email_error(self, pyramid_request):
form = forms.RegistrationForm(
request=pyramid_request,
formdata=MultiDict({"email": "foo@bearsarefuzzy.com"}),
user_service=pretend.stub(
find_userid_by_email=pretend.call_recorder(lambda _: None)
Expand All @@ -545,8 +561,30 @@ def test_prohibited_email_error(self, pyramid_config):
"different email."
)

def test_prohibited_email_error(self, db_request):
domain = ProhibitedEmailDomain(domain="wutang.net")
db_request.db.add(domain)

form = forms.RegistrationForm(
request=db_request,
formdata=MultiDict({"email": "foo@wutang.net"}),
user_service=pretend.stub(
find_userid_by_email=pretend.call_recorder(lambda _: None)
),
captcha_service=pretend.stub(enabled=True),
breach_service=pretend.stub(check_password=lambda pw, tags=None: False),
)

assert not form.validate()
assert (
str(form.email.errors.pop())
== "You can't use an email address from this domain. Use a "
"different email."
)

def test_recaptcha_disabled(self):
form = forms.RegistrationForm(
request=pretend.stub(),
formdata=MultiDict({"g_recpatcha_response": ""}),
user_service=pretend.stub(),
captcha_service=pretend.stub(
Expand All @@ -562,6 +600,7 @@ def test_recaptcha_disabled(self):

def test_recaptcha_required_error(self):
form = forms.RegistrationForm(
request=pretend.stub(),
formdata=MultiDict({"g_recaptcha_response": ""}),
user_service=pretend.stub(),
captcha_service=pretend.stub(
Expand All @@ -575,6 +614,7 @@ def test_recaptcha_required_error(self):

def test_recaptcha_error(self):
form = forms.RegistrationForm(
request=pretend.stub(),
formdata=MultiDict({"g_recaptcha_response": "asd"}),
user_service=pretend.stub(),
captcha_service=pretend.stub(
Expand All @@ -588,6 +628,7 @@ def test_recaptcha_error(self):

def test_username_exists(self, pyramid_config):
form = forms.RegistrationForm(
request=pretend.stub(),
formdata=MultiDict({"username": "foo"}),
user_service=pretend.stub(
find_userid=pretend.call_recorder(lambda name: 1),
Expand All @@ -608,6 +649,7 @@ def test_username_exists(self, pyramid_config):

def test_username_prohibted(self, pyramid_config):
form = forms.RegistrationForm(
request=pretend.stub(),
formdata=MultiDict({"username": "foo"}),
user_service=pretend.stub(
username_is_prohibited=lambda a: True,
Expand All @@ -628,6 +670,7 @@ def test_username_prohibted(self, pyramid_config):
@pytest.mark.parametrize("username", ["_foo", "bar_", "foo^bar", "boo\0far"])
def test_username_is_valid(self, username, pyramid_config):
form = forms.RegistrationForm(
request=pretend.stub(),
formdata=MultiDict({"username": username}),
user_service=pretend.stub(
find_userid=pretend.call_recorder(lambda _: None),
Expand Down Expand Up @@ -656,6 +699,7 @@ def test_password_strength(self):
)
for pwd, valid in cases:
form = forms.RegistrationForm(
request=pretend.stub(),
formdata=MultiDict({"new_password": pwd, "password_confirm": pwd}),
user_service=pretend.stub(),
captcha_service=pretend.stub(
Expand All @@ -669,6 +713,7 @@ def test_password_strength(self):

def test_password_breached(self):
form = forms.RegistrationForm(
request=pretend.stub(),
formdata=MultiDict({"new_password": "password"}),
user_service=pretend.stub(
find_userid=pretend.call_recorder(lambda _: None)
Expand All @@ -693,6 +738,7 @@ def test_password_breached(self):

def test_name_too_long(self, pyramid_config):
form = forms.RegistrationForm(
request=pretend.stub(),
formdata=MultiDict({"full_name": "hello " * 50}),
user_service=pretend.stub(
find_userid=pretend.call_recorder(lambda _: None)
Expand Down Expand Up @@ -720,6 +766,7 @@ class TestRequestPasswordResetForm:
)
def test_validate(self, form_input):
form = forms.RequestPasswordResetForm(
request=pretend.stub(),
formdata=MultiDict({"username_or_email": form_input}),
)
assert form.validate()
Expand Down
7 changes: 7 additions & 0 deletions tests/unit/admin/test_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ def test_includeme():
factory="warehouse.accounts.models:UserFactory",
traverse="/{username}",
),
pretend.call(
"admin.user.freeze",
"/admin/users/{username}/freeze/",
domain=warehouse,
factory="warehouse.accounts.models:UserFactory",
traverse="/{username}",
),
pretend.call(
"admin.user.reset_password",
"/admin/users/{username}/reset_password/",
Expand Down
60 changes: 60 additions & 0 deletions tests/unit/admin/views/test_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from warehouse.accounts.interfaces import IEmailBreachedService, IUserService
from warehouse.accounts.models import (
DisableReason,
ProhibitedEmailDomain,
ProhibitedUserName,
RecoveryCode,
WebAuthn,
Expand Down Expand Up @@ -396,6 +397,65 @@ def test_user_delete_redirects_actual_name(self, db_request):
]


class TestUserFreeze:
def test_freezes_user(self, db_request, monkeypatch):
user = UserFactory.create()
verified_email = EmailFactory.create(user=user, verified=True, primary=True)
EmailFactory.create(user=user, verified=False, primary=False)

db_request.matchdict["username"] = str(user.username)
db_request.params = {"username": user.username}
db_request.route_path = pretend.call_recorder(lambda a: "/foobar")
db_request.user = UserFactory.create()

result = views.user_freeze(user, db_request)

db_request.db.flush()

assert db_request.db.get(User, user.id).is_frozen
prohibition = db_request.db.query(ProhibitedEmailDomain).one()
assert prohibition.domain == verified_email.domain

assert db_request.route_path.calls == [pretend.call("admin.user.list")]
assert result.status_code == 303
assert result.location == "/foobar"

def test_freezes_user_bad_confirm(self, db_request, monkeypatch):
user = UserFactory.create(is_frozen=False)
EmailFactory.create(user=user, verified=True, primary=True)

db_request.matchdict["username"] = str(user.username)
db_request.params = {"username": "wrong"}
db_request.route_path = pretend.call_recorder(lambda a, **k: "/foobar")

result = views.user_freeze(user, db_request)

db_request.db.flush()

assert not db_request.db.get(User, user.id).is_frozen
assert not db_request.db.query(ProhibitedEmailDomain).all()
assert db_request.route_path.calls == [
pretend.call("admin.user.detail", username=user.username)
]
assert result.status_code == 303
assert result.location == "/foobar"

def test_user_freeze_redirects_actual_name(self, db_request):
user = UserFactory.create(username="wu-tang")
db_request.matchdict["username"] = "Wu-Tang"
db_request.current_route_path = pretend.call_recorder(
lambda username: "/user/the-redirect/"
)

result = views.user_freeze(user, db_request)

assert isinstance(result, HTTPMovedPermanently)
assert result.headers["Location"] == "/user/the-redirect/"
assert db_request.current_route_path.calls == [
pretend.call(username=user.username)
]


class TestUserResetPassword:
def test_resets_password(self, db_request, monkeypatch):
user = UserFactory.create()
Expand Down
24 changes: 21 additions & 3 deletions tests/unit/manage/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ def test_validate(self):
user_id = pretend.stub()
user_service = pretend.stub(find_userid_by_email=lambda _: None)
form = forms.AddEmailForm(
request=pretend.stub(
db=pretend.stub(query=lambda *a: pretend.stub(scalar=lambda: False))
),
formdata=MultiDict({"email": "foo@bar.com"}),
user_id=user_id,
user_service=user_service,
Expand All @@ -182,9 +185,13 @@ def test_validate(self):
assert form.user_service is user_service
assert form.validate(), str(form.errors)

def test_email_exists_error(self, pyramid_config):
def test_email_exists_error(self, pyramid_request):
pyramid_request.db = pretend.stub(
query=lambda *a: pretend.stub(scalar=lambda: False)
)
user_id = pretend.stub()
form = forms.AddEmailForm(
request=pyramid_request,
formdata=MultiDict({"email": "foo@bar.com"}),
user_id=user_id,
user_service=pretend.stub(find_userid_by_email=lambda _: user_id),
Expand All @@ -197,8 +204,12 @@ def test_email_exists_error(self, pyramid_config):
"Use a different email."
)

def test_email_exists_other_account_error(self, pyramid_config):
def test_email_exists_other_account_error(self, pyramid_request):
pyramid_request.db = pretend.stub(
query=lambda *a: pretend.stub(scalar=lambda: False)
)
form = forms.AddEmailForm(
request=pyramid_request,
formdata=MultiDict({"email": "foo@bar.com"}),
user_id=pretend.stub(),
user_service=pretend.stub(find_userid_by_email=lambda _: pretend.stub()),
Expand All @@ -211,8 +222,12 @@ def test_email_exists_other_account_error(self, pyramid_config):
"Use a different email."
)

def test_prohibited_email_error(self, pyramid_config):
def test_prohibited_email_error(self, pyramid_request):
pyramid_request.db = pretend.stub(
query=lambda *a: pretend.stub(scalar=lambda: False)
)
form = forms.AddEmailForm(
request=pyramid_request,
formdata=MultiDict({"email": "foo@bearsarefuzzy.com"}),
user_service=pretend.stub(find_userid_by_email=lambda _: None),
user_id=pretend.stub(),
Expand All @@ -227,6 +242,9 @@ def test_prohibited_email_error(self, pyramid_config):

def test_email_too_long_error(self, pyramid_config):
form = forms.AddEmailForm(
request=pretend.stub(
db=pretend.stub(query=lambda *a: pretend.stub(scalar=lambda: False))
),
formdata=MultiDict({"email": f"{'x' * 300}@bar.com"}),
user_service=pretend.stub(find_userid_by_email=lambda _: None),
user_id=pretend.stub(),
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/manage/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def test_default_response(self, monkeypatch, public_email, expected_public_email
)
]
assert add_email_cls.calls == [
pretend.call(user_id=user_id, user_service=user_service)
pretend.call(request=request, user_id=user_id, user_service=user_service)
]
assert change_pass_cls.calls == [
pretend.call(
Expand Down

0 comments on commit 7d255a3

Please sign in to comment.