Skip to content

Commit 17b390e

Browse files
committed
Merge branch 'release/2.025.37'
2 parents d4baca9 + 3c686ec commit 17b390e

File tree

5 files changed

+128
-7
lines changed

5 files changed

+128
-7
lines changed

kobo/apps/accounts/apps.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,9 @@
66
class AccountExtrasConfig(AppConfig):
77
name = 'kobo.apps.accounts'
88
verbose_name = 'Account Extras'
9+
10+
def ready(self):
11+
# Makes sure all signal handlers are connected
12+
from kobo.apps.accounts import signals
13+
14+
super().ready()

kobo/apps/accounts/serializers.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,18 @@ def create(self, validated_data):
2828
def validate(self, attrs):
2929
"""
3030
Validates that only owners or admins of the organization can update
31-
their email
31+
their email and only if they don't have an SSO-provided email
3232
"""
33-
user = self.context['request'].user
33+
request = self.context['request']
34+
user = request.user
3435
organization = user.organization
36+
# check if we have an SSO-provided email
37+
# assume if we have an email address and an SSO account then the email comes
38+
# from the SSO
39+
if user.socialaccount_set.exists() and user.emailaddress_set.exists():
40+
raise serializers.ValidationError(
41+
{'email': t('This action is not allowed.')}
42+
)
3543
if organization.is_owner(user) or organization.is_admin(user):
3644
return attrs
3745
raise serializers.ValidationError(
@@ -63,11 +71,11 @@ class Meta:
6371
@extend_schema_field(OpenApiTypes.EMAIL)
6472
def get_email(self, obj):
6573
if obj.extra_data:
66-
if "email" in obj.extra_data:
67-
return obj.extra_data.get("email")
68-
return obj.extra_data.get("userPrincipalName") # MS oauth uses this
74+
if 'email' in obj.extra_data:
75+
return obj.extra_data.get('email')
76+
return obj.extra_data.get('userPrincipalName') # MS oauth uses this
6977

7078
@extend_schema_field(OpenApiTypes.STR)
7179
def get_username(self, obj):
7280
if obj.extra_data:
73-
return obj.extra_data.get("username")
81+
return obj.extra_data.get('username')

kobo/apps/accounts/signals.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from allauth.account.models import EmailAddress
2+
from allauth.account.signals import email_confirmed
3+
from allauth.account.utils import cleanup_email_addresses
4+
from allauth.socialaccount.signals import social_account_added
5+
from django.dispatch import receiver
6+
7+
8+
@receiver(social_account_added)
9+
def update_email(*args, **kwargs):
10+
sociallogin = kwargs.get('sociallogin')
11+
request = kwargs.get('request')
12+
social_email_addresses = sociallogin.email_addresses
13+
# if the provider doesn't use email, don't bother updating addresses
14+
if not social_email_addresses:
15+
return
16+
social_user = sociallogin.user
17+
for social_email in social_email_addresses:
18+
# the auto-created EmailAddresses don't have the user already attached (?!)
19+
social_email.user = social_user
20+
existing_email_addresses = list(EmailAddress.objects.filter(user=social_user))
21+
# put the social email addresses first so they get marked as primary
22+
all_email_addresses = [*social_email_addresses, *existing_email_addresses]
23+
emails, primary = cleanup_email_addresses(request, all_email_addresses)
24+
25+
# cleanup_email_addresses doesn't actually call set_as_primary on the primary
26+
# email so do that now
27+
primary.set_as_primary()
28+
29+
# update existing emails to reflect that they are no longer primary
30+
# and add any new emails from the SocialLogin
31+
EmailAddress.objects.bulk_create(
32+
emails,
33+
update_conflicts=True,
34+
unique_fields=['email','user_id'],
35+
update_fields=['primary']
36+
)
37+
# for some reason allauth doesn't emit the email confirmed signal even
38+
# though if we're calling the social_account_added signal, the email has been
39+
# verified
40+
email_confirmed.send(
41+
sender=EmailAddress,
42+
request=request,
43+
email_address=primary,
44+
)

kobo/apps/accounts/tests/test_email.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from allauth.account.models import EmailAddress
2+
from ddt import data, ddt
13
from django.conf import settings
24
from django.core import mail
35
from django.urls import reverse
@@ -84,7 +86,7 @@ def test_new_confirm_email(self):
8486
confirm_url = line.split('testserver')[1].rsplit('/', 1)[0]
8587
queries = FuzzyInt(15, 20)
8688
with self.assertNumQueries(queries):
87-
res = self.client.post(confirm_url + "/")
89+
res = self.client.post(confirm_url + '/')
8890
self.assertEqual(res.status_code, 302)
8991
self.assertTrue(
9092
self.user.emailaddress_set.filter(
@@ -101,6 +103,7 @@ def test_new_confirm_email(self):
101103
)
102104

103105

106+
@ddt
104107
class EmailUpdateRestrictionTestCase(APITestCase):
105108
"""
106109
Test that only organization owners and admins can update their email.
@@ -171,3 +174,28 @@ def test_that_non_mmo_user_can_update_email(self):
171174
).count(),
172175
1
173176
)
177+
178+
@data('mmo_admin', 'mmo_owner', 'mmo_member', 'non_mmo_user')
179+
def test_that_user_cannot_update_email_if_sso(self, user_type):
180+
if user_type == 'mmo_admin':
181+
user = self.admin
182+
elif user_type == 'mmo_owner':
183+
user = self.owner
184+
elif user_type == 'mmo_member':
185+
user = self.member
186+
else:
187+
user = self.non_mmo_user
188+
baker.make('socialaccount.SocialAccount', user=user)
189+
# in real life connecting the social account would have made an EmailAddress
190+
email_address = baker.make('account.emailaddress', user=user)
191+
self.assertNotEqual(email_address.email, '[email protected]')
192+
self.client.force_login(user)
193+
data = {'email': '[email protected]'}
194+
res = self.client.post(self.url_list, data, format='json')
195+
self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
196+
# just check that the EmailAddress objects haven't changed;
197+
# user.email relies on signal handlers so is tested in test_signals
198+
self.assertEqual(user.emailaddress_set.count(), 1)
199+
user_email = EmailAddress.objects.get(user=user)
200+
self.assertEqual(user_email.email, email_address.email)
201+
self.assertEqual(user.emailaddress_set.count(), 1)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from allauth.account.models import EmailAddress
2+
from allauth.socialaccount.models import SocialLogin
3+
from django.test import RequestFactory, TestCase
4+
5+
from kobo.apps.accounts.signals import update_email
6+
from kobo.apps.kobo_auth.shortcuts import User
7+
8+
9+
class TestAccountSignals(TestCase):
10+
fixtures = ['test_data']
11+
def setUp(self):
12+
self.user = User.objects.get(username='someuser')
13+
self.email_address = EmailAddress.objects.create(
14+
user=self.user,
15+
email=self.user.email,
16+
verified=True,
17+
primary=True
18+
)
19+
20+
def test_social_login_connect_updates_primary_email(self):
21+
request = RequestFactory().get('/')
22+
new_address = EmailAddress(
23+
24+
verified=True,
25+
primary=True
26+
)
27+
social_login = SocialLogin(email_addresses=[new_address], user=self.user)
28+
update_email(request=request, sociallogin=social_login)
29+
# we should have gotten rid of any old EmailAddresses
30+
assert EmailAddress.objects.count() == 1
31+
found_address = EmailAddress.objects.first()
32+
assert found_address.email == new_address.email
33+
assert found_address.verified
34+
assert found_address.primary
35+
assert self.user.email == new_address.email

0 commit comments

Comments
 (0)