Merge pull request #1493 from cisagov/dk/1458-uppercase-emails

Issue #1458 - lowercase emails on add user to domain; case insensitive match
This commit is contained in:
dave-kennedy-ecs 2023-12-14 11:16:52 -05:00 committed by GitHub
commit b657db8821
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 69 additions and 18 deletions

View file

@ -28,6 +28,17 @@ class DomainAddUserForm(forms.Form):
email = forms.EmailField(label="Email") email = forms.EmailField(label="Email")
def clean(self):
"""clean form data by lowercasing email"""
cleaned_data = super().clean()
# Lowercase the value of the 'email' field
email_value = cleaned_data.get("email")
if email_value:
cleaned_data["email"] = email_value.lower()
return cleaned_data
class DomainNameserverForm(forms.Form): class DomainNameserverForm(forms.Form):
"""Form for changing nameservers.""" """Form for changing nameservers."""

View file

@ -101,7 +101,7 @@ class User(AbstractUser):
"""When a user first arrives on the site, we need to retrieve any domain """When a user first arrives on the site, we need to retrieve any domain
invitations that match their email address.""" invitations that match their email address."""
for invitation in DomainInvitation.objects.filter( for invitation in DomainInvitation.objects.filter(
email=self.email, status=DomainInvitation.DomainInvitationStatus.INVITED email__iexact=self.email, status=DomainInvitation.DomainInvitationStatus.INVITED
): ):
try: try:
invitation.retrieve() invitation.retrieve()

View file

@ -654,3 +654,17 @@ class TestUser(TestCase):
"""A new user who's neither transitioned nor invited should """A new user who's neither transitioned nor invited should
return True when tested with class method needs_identity_verification""" return True when tested with class method needs_identity_verification"""
self.assertTrue(User.needs_identity_verification(self.user.email, self.user.username)) self.assertTrue(User.needs_identity_verification(self.user.email, self.user.username))
def test_check_domain_invitations_on_login_caps_email(self):
"""A DomainInvitation with an email address with capital letters should match
a User record whose email address is not in caps"""
# create DomainInvitation with CAPS email that matches User email
# on a case-insensitive match
caps_email = "MAYOR@igorville.gov"
# mock the domain invitation save routine
with patch("registrar.models.DomainInvitation.save") as save_mock:
DomainInvitation.objects.get_or_create(email=caps_email, domain=self.domain)
self.user.check_domain_invitations_on_login()
# if check_domain_invitations_on_login properly matches exactly one
# Domain Invitation, then save routine should be called exactly once
save_mock.assert_called_once()

View file

@ -1355,29 +1355,55 @@ class TestDomainManagers(TestDomainOverview):
out the boto3 SES email sending here. out the boto3 SES email sending here.
""" """
# make sure there is no user with this email # make sure there is no user with this email
EMAIL = "mayor@igorville.gov" email_address = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete() User.objects.filter(email=email_address).delete()
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
add_page.form["email"] = EMAIL add_page.form["email"] = email_address
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
success_result = add_page.form.submit() success_result = add_page.form.submit()
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
success_page = success_result.follow() success_page = success_result.follow()
self.assertContains(success_page, EMAIL) self.assertContains(success_page, email_address)
self.assertContains(success_page, "Cancel") # link to cancel invitation self.assertContains(success_page, "Cancel") # link to cancel invitation
self.assertTrue(DomainInvitation.objects.filter(email=EMAIL).exists()) self.assertTrue(DomainInvitation.objects.filter(email=email_address).exists())
@boto3_mocking.patching
def test_domain_invitation_created_for_caps_email(self):
"""Add user on a nonexistent email with CAPS creates an invitation to lowercase email.
Adding a non-existent user sends an email as a side-effect, so mock
out the boto3 SES email sending here.
"""
# make sure there is no user with this email
email_address = "mayor@igorville.gov"
caps_email_address = "MAYOR@igorville.gov"
User.objects.filter(email=email_address).delete()
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
add_page.form["email"] = caps_email_address
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
success_result = add_page.form.submit()
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
success_page = success_result.follow()
self.assertContains(success_page, email_address)
self.assertContains(success_page, "Cancel") # link to cancel invitation
self.assertTrue(DomainInvitation.objects.filter(email=email_address).exists())
@boto3_mocking.patching @boto3_mocking.patching
def test_domain_invitation_email_sent(self): def test_domain_invitation_email_sent(self):
"""Inviting a non-existent user sends them an email.""" """Inviting a non-existent user sends them an email."""
# make sure there is no user with this email # make sure there is no user with this email
EMAIL = "mayor@igorville.gov" email_address = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete() User.objects.filter(email=email_address).delete()
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
@ -1386,28 +1412,28 @@ class TestDomainManagers(TestDomainOverview):
with boto3_mocking.clients.handler_for("sesv2", mock_client): with boto3_mocking.clients.handler_for("sesv2", mock_client):
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
add_page.form["email"] = EMAIL add_page.form["email"] = email_address
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
add_page.form.submit() add_page.form.submit()
# check the mock instance to see if `send_email` was called right # check the mock instance to see if `send_email` was called right
mock_client_instance.send_email.assert_called_once_with( mock_client_instance.send_email.assert_called_once_with(
FromEmailAddress=settings.DEFAULT_FROM_EMAIL, FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
Destination={"ToAddresses": [EMAIL]}, Destination={"ToAddresses": [email_address]},
Content=ANY, Content=ANY,
) )
def test_domain_invitation_cancel(self): def test_domain_invitation_cancel(self):
"""Posting to the delete view deletes an invitation.""" """Posting to the delete view deletes an invitation."""
EMAIL = "mayor@igorville.gov" email_address = "mayor@igorville.gov"
invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=EMAIL) invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=email_address)
self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id})) self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id}))
with self.assertRaises(DomainInvitation.DoesNotExist): with self.assertRaises(DomainInvitation.DoesNotExist):
DomainInvitation.objects.get(id=invitation.id) DomainInvitation.objects.get(id=invitation.id)
def test_domain_invitation_cancel_no_permissions(self): def test_domain_invitation_cancel_no_permissions(self):
"""Posting to the delete view as a different user should fail.""" """Posting to the delete view as a different user should fail."""
EMAIL = "mayor@igorville.gov" email_address = "mayor@igorville.gov"
invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=EMAIL) invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=email_address)
other_user = User() other_user = User()
other_user.save() other_user.save()
@ -1419,20 +1445,20 @@ class TestDomainManagers(TestDomainOverview):
@boto3_mocking.patching @boto3_mocking.patching
def test_domain_invitation_flow(self): def test_domain_invitation_flow(self):
"""Send an invitation to a new user, log in and load the dashboard.""" """Send an invitation to a new user, log in and load the dashboard."""
EMAIL = "mayor@igorville.gov" email_address = "mayor@igorville.gov"
User.objects.filter(email=EMAIL).delete() User.objects.filter(email=email_address).delete()
add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id}))
self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain)
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
add_page.form["email"] = EMAIL add_page.form["email"] = email_address
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
add_page.form.submit() add_page.form.submit()
# user was invited, create them # user was invited, create them
new_user = User.objects.create(username=EMAIL, email=EMAIL) new_user = User.objects.create(username=email_address, email=email_address)
# log them in to `self.app` # log them in to `self.app`
self.app.set_user(new_user.username) self.app.set_user(new_user.username)
# and manually call the on each login callback # and manually call the on each login callback