Skip to content

Commit c6fbdef

Browse files
fix: Handle integrity violations when confirming email address (ietf-tools#5506)
* fix: Handle integrity violations when confirming email address * test: Add tests of confirm_new_email view
1 parent 5f1f7aa commit c6fbdef

3 files changed

Lines changed: 91 additions & 5 deletions

File tree

ietf/ietfauth/tests.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,11 @@
2424
from unittest import skipIf
2525
from urllib.parse import urlsplit
2626

27+
import django.core.signing
2728
from django.urls import reverse as urlreverse
2829
from django.contrib.auth.models import User
2930
from django.conf import settings
30-
from django.template.loader import render_to_string
31+
from django.template.loader import render_to_string
3132
from django.utils import timezone
3233

3334
import debug # pyflakes:ignore
@@ -937,6 +938,72 @@ def test_edit_person_extresources(self):
937938
self.assertEqual(person.personextresource_set.get(name__slug='github_repo').display_name, 'Some display text')
938939
self.assertIn(person.personextresource_set.first().name.slug, str(person.personextresource_set.first()))
939940

941+
def test_confirm_new_email(self):
942+
person = PersonFactory()
943+
valid_auth = django.core.signing.dumps(
944+
[person.user.username, "new_email@example.com"], salt="add_email"
945+
)
946+
invalid_auth = django.core.signing.dumps(
947+
[person.user.username, "not_this_one@example.com"], salt="pepper"
948+
)
949+
950+
# Test that we check the salt
951+
r = self.client.get(
952+
urlreverse("ietf.ietfauth.views.confirm_new_email", kwargs={"auth": invalid_auth})
953+
)
954+
self.assertEqual(r.status_code, 404)
955+
r = self.client.post(
956+
urlreverse("ietf.ietfauth.views.confirm_new_email", kwargs={"auth": invalid_auth})
957+
)
958+
self.assertEqual(r.status_code, 404)
959+
960+
# Now check that the valid auth works
961+
self.assertFalse(
962+
person.email_set.filter(address__icontains="new_email@example.com").exists()
963+
)
964+
confirm_url = urlreverse(
965+
"ietf.ietfauth.views.confirm_new_email", kwargs={"auth": valid_auth}
966+
)
967+
r = self.client.get(confirm_url)
968+
self.assertContains(r, urllib.parse.quote(confirm_url), status_code=200)
969+
r = self.client.post(confirm_url, data={"action": "confirm"})
970+
self.assertContains(r, "has been updated", status_code=200)
971+
self.assertTrue(
972+
person.email_set.filter(address__icontains="new_email@example.com").exists()
973+
)
974+
975+
# Authorizing a second time should be handled gracefully
976+
r = self.client.post(confirm_url, data={"action": "confirm"})
977+
self.assertContains(r, "already includes", status_code=200)
978+
979+
# Another person should not be able to add the same address and should be told so,
980+
# whether they use the same or different letter case
981+
other_person = PersonFactory()
982+
other_auth = django.core.signing.dumps(
983+
[other_person.user.username, "new_email@example.com"], salt="add_email"
984+
)
985+
r = self.client.post(
986+
urlreverse("ietf.ietfauth.views.confirm_new_email", kwargs={"auth": other_auth}),
987+
data={"action": "confirm"},
988+
)
989+
self.assertContains(r, "in use by another user", status_code=200)
990+
991+
other_auth = django.core.signing.dumps(
992+
[other_person.user.username, "NeW_eMaIl@eXaMpLe.CoM"], salt="add_email"
993+
)
994+
r = self.client.post(
995+
urlreverse("ietf.ietfauth.views.confirm_new_email", kwargs={"auth": other_auth}),
996+
data={"action": "confirm"},
997+
)
998+
999+
self.assertContains(r, "in use by another user", status_code=200)
1000+
self.assertFalse(
1001+
other_person.email_set.filter(address__icontains="new_email@example.com").exists()
1002+
)
1003+
self.assertTrue(
1004+
person.email_set.filter(address__icontains="new_email@example.com").exists()
1005+
)
1006+
9401007

9411008
class OpenIDConnectTests(TestCase):
9421009
def request_matcher(self, request):

ietf/ietfauth/views.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
from django.contrib.auth.views import LoginView
5454
from django.contrib.sites.models import Site
5555
from django.core.exceptions import ObjectDoesNotExist, ValidationError
56+
from django.db import IntegrityError
5657
from django.urls import reverse as urlreverse
5758
from django.http import Http404, HttpResponseRedirect, HttpResponseForbidden
5859
from django.shortcuts import render, redirect, get_object_or_404
@@ -440,15 +441,27 @@ def confirm_new_email(request, auth):
440441
form = NewEmailForm({ "new_email": email })
441442
can_confirm = form.is_valid() and email
442443
new_email_obj = None
444+
created = False
443445
if request.method == 'POST' and can_confirm and request.POST.get("action") == "confirm":
444-
new_email_obj = Email.objects.create(address=email, person=person, origin=username)
446+
try:
447+
new_email_obj, created = Email.objects.get_or_create(
448+
address=email,
449+
person=person,
450+
defaults={'origin': username},
451+
)
452+
except IntegrityError:
453+
can_confirm = False
454+
form.add_error(
455+
None, "Email address is in use by another user. Please contact the secretariat for assistance."
456+
)
445457

446458
return render(request, 'registration/confirm_new_email.html', {
447459
'username': username,
448460
'email': email,
449461
'can_confirm': can_confirm,
450462
'form': form,
451463
'new_email_obj': new_email_obj,
464+
'already_confirmed': new_email_obj and not created,
452465
})
453466

454467
def password_reset(request):

ietf/templates/registration/confirm_new_email.html

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,15 @@ <h1>Confirm new email address</h1>
1313
<a class="btn btn-primary my-3"
1414
href="{% url "ietf.ietfauth.views.profile" %}">Edit profile</a>
1515
{% elif new_email_obj %}
16-
<p class="alert alert-success my-3">
17-
Your account {{ username }} has been updated to include the email address {{ email|linkify }}.
18-
</p>
16+
{% if already_confirmed %}
17+
<p class="alert alert-info my-3">
18+
Your account {{ username }} already includes the email address {{ email|linkify }}.
19+
</p>
20+
{% else %}
21+
<p class="alert alert-success my-3">
22+
Your account {{ username }} has been updated to include the email address {{ email|linkify }}.
23+
</p>
24+
{% endif %}
1925
<a class="btn btn-primary my-3"
2026
href="{% url "ietf.ietfauth.views.profile" %}">Edit profile</a>
2127
{% else %}

0 commit comments

Comments
 (0)