Skip to content

Commit

Permalink
Merge pull request python-discord#292 from python-discord/allauth-use…
Browse files Browse the repository at this point in the history
…r-settings

Allauth improvements & GH
  • Loading branch information
scragly authored Nov 11, 2019
2 parents 8cc1f03 + afc014e commit cf68b3f
Show file tree
Hide file tree
Showing 22 changed files with 1,052 additions and 34 deletions.
5 changes: 5 additions & 0 deletions pydis_site/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@

# Remove the toc header prefix. There's no option for this, so we gotta monkey patch it.
toc.HEADER_ID_PREFIX = ''

# Empty list of validators for Allauth to ponder over. This is referred to in settings.py
# by a string because Allauth won't let us just give it a list _there_, we have to point
# at a list _somewhere else_ instead.
VALIDATORS = []
Empty file.
24 changes: 24 additions & 0 deletions pydis_site/apps/home/forms/account_deletion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout
from django.forms import CharField, Form
from django_crispy_bulma.layout import IconField, Submit


class AccountDeletionForm(Form):
"""Account deletion form, to collect username for confirmation of removal."""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()

self.helper.form_method = "post"
self.helper.add_input(Submit("submit", "I understand, delete my account"))

self.helper.layout = Layout(
IconField("username", icon_prepend="user")
)

username = CharField(
label="Username",
required=True
)
84 changes: 69 additions & 15 deletions pydis_site/apps/home/signals.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from contextlib import suppress
from typing import List, Optional, Type

from allauth.account.signals import user_logged_in
Expand All @@ -8,7 +9,7 @@
pre_social_login, social_account_added, social_account_removed,
social_account_updated)
from django.contrib.auth.models import Group, User as DjangoUser
from django.db.models.signals import post_save, pre_delete, pre_save
from django.db.models.signals import post_delete, post_save, pre_save

from pydis_site.apps.api.models import User as DiscordUser
from pydis_site.apps.staff.models import RoleMapping
Expand Down Expand Up @@ -37,7 +38,7 @@ class AllauthSignalListener:
def __init__(self):
post_save.connect(self.user_model_updated, sender=DiscordUser)

pre_delete.connect(self.mapping_model_deleted, sender=RoleMapping)
post_delete.connect(self.mapping_model_deleted, sender=RoleMapping)
pre_save.connect(self.mapping_model_updated, sender=RoleMapping)

pre_social_login.connect(self.social_account_updated)
Expand Down Expand Up @@ -133,13 +134,29 @@ def mapping_model_deleted(self, sender: Type[RoleMapping], **kwargs) -> None:
Processes deletion signals from the RoleMapping model, removing perms from users.
We need to do this to ensure that users aren't left with permissions groups that
they shouldn't have assigned to them when a RoleMapping is deleted from the database.
they shouldn't have assigned to them when a RoleMapping is deleted from the database,
and to remove their staff status if they should no longer have it.
"""
instance: RoleMapping = kwargs["instance"]

for user in instance.group.user_set.all():
# Firstly, remove their related user group
user.groups.remove(instance.group)

with suppress(SocialAccount.DoesNotExist, DiscordUser.DoesNotExist):
# If we get either exception, then the user could not have been assigned staff
# with our system in the first place.

social_account = SocialAccount.objects.get(user=user, provider=DiscordProvider.id)
discord_user = DiscordUser.objects.get(id=int(social_account.uid))

mappings = RoleMapping.objects.filter(role__in=discord_user.roles.all()).all()
is_staff = any(m.is_staff for m in mappings)

if user.is_staff != is_staff:
user.is_staff = is_staff
user.save(update_fields=("is_staff", ))

def mapping_model_updated(self, sender: Type[RoleMapping], **kwargs) -> None:
"""
Processes update signals from the RoleMapping model.
Expand Down Expand Up @@ -174,6 +191,21 @@ def mapping_model_updated(self, sender: Type[RoleMapping], **kwargs) -> None:
for account in accounts:
account.user.groups.add(instance.group)

if instance.is_staff and not account.user.is_staff:
account.user.is_staff = instance.is_staff
account.user.save(update_fields=("is_staff", ))
else:
discord_user = DiscordUser.objects.get(id=int(account.uid))

mappings = RoleMapping.objects.filter(
role__in=discord_user.roles.all()
).exclude(id=instance.id).all()
is_staff = any(m.is_staff for m in mappings)

if account.user.is_staff != is_staff:
account.user.is_staff = is_staff
account.user.save(update_fields=("is_staff",))

def user_model_updated(self, sender: Type[DiscordUser], **kwargs) -> None:
"""
Processes update signals from the Discord User model, assigning perms as required.
Expand Down Expand Up @@ -230,31 +262,53 @@ def _apply_groups(
except SocialAccount.user.RelatedObjectDoesNotExist:
return # There's no user account yet, this will be handled by another receiver

# Ensure that the username on this account is correct
new_username = f"{user.name}#{user.discriminator}"

if account.user.username != new_username:
account.user.username = new_username
account.user.first_name = new_username

if not user.in_guild:
deletion = True

if deletion:
# They've unlinked Discord or left the server, so we have to remove their groups
# and their staff status

if not current_groups:
return # They have no groups anyway, no point in processing
if current_groups:
# They do have groups, so let's remove them
account.user.groups.remove(
*(mapping.group for mapping in mappings)
)

account.user.groups.remove(
*(mapping.group for mapping in mappings)
)
if account.user.is_staff:
# They're marked as a staff user and they shouldn't be, so let's fix that
account.user.is_staff = False
else:
new_groups = []
is_staff = False

for role in user.roles.all():
try:
new_groups.append(mappings.get(role=role).group)
mapping = mappings.get(role=role)
except RoleMapping.DoesNotExist:
continue # No mapping exists

account.user.groups.add(
*[group for group in new_groups if group not in current_groups]
)
new_groups.append(mapping.group)

account.user.groups.remove(
*[mapping.group for mapping in mappings if mapping.group not in new_groups]
)
if mapping.is_staff:
is_staff = True

account.user.groups.add(
*[group for group in new_groups if group not in current_groups]
)

account.user.groups.remove(
*[mapping.group for mapping in mappings if mapping.group not in new_groups]
)

if account.user.is_staff != is_staff:
account.user.is_staff = is_staff

account.user.save()
70 changes: 66 additions & 4 deletions pydis_site/apps/home/tests/test_signal_listener.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,14 @@ def setUpTestData(cls):

cls.admin_mapping = RoleMapping.objects.create(
role=cls.admin_role,
group=cls.admin_group
group=cls.admin_group,
is_staff=True
)

cls.moderator_mapping = RoleMapping.objects.create(
role=cls.moderator_role,
group=cls.moderator_group
group=cls.moderator_group,
is_staff=False
)

cls.discord_user = DiscordUser.objects.create(
Expand Down Expand Up @@ -166,7 +168,7 @@ def setUpTestData(cls):

cls.django_moderator = DjangoUser.objects.create(
username="moderator",
is_staff=True,
is_staff=False,
is_superuser=False
)

Expand Down Expand Up @@ -339,6 +341,33 @@ def test_apply_groups_admin(self):
self.discord_admin.roles.add(self.admin_role)
self.discord_admin.save()

def test_apply_groups_moderator(self):
"""Test application of groups by role, relating to a non-`is_staff` moderator user."""
handler = AllauthSignalListener()

self.assertEqual(self.django_user_discordless.groups.all().count(), 0)

# Apply groups based on moderator role being present on Discord
handler._apply_groups(self.discord_moderator, self.social_moderator)
self.assertTrue(self.moderator_group in self.django_moderator.groups.all())

# Remove groups based on the user apparently leaving the server
handler._apply_groups(self.discord_moderator, self.social_moderator, True)
self.assertEqual(self.django_user_discordless.groups.all().count(), 0)

# Apply the moderator role again
handler._apply_groups(self.discord_moderator, self.social_moderator)

# Remove all of the roles from the user
self.discord_moderator.roles.clear()

# Remove groups based on the user no longer having the moderator role on Discord
handler._apply_groups(self.discord_moderator, self.social_moderator)
self.assertEqual(self.django_user_discordless.groups.all().count(), 0)

self.discord_moderator.roles.add(self.moderator_role)
self.discord_moderator.save()

def test_apply_groups_other(self):
"""Test application of groups by role, relating to non-standard cases."""
handler = AllauthSignalListener()
Expand Down Expand Up @@ -373,10 +402,25 @@ def test_role_mapping_changes(self):
self.assertEqual(self.django_moderator.groups.all().count(), 1)
self.assertEqual(self.django_admin.groups.all().count(), 1)

# Test is_staff changes
self.admin_mapping.is_staff = False
self.admin_mapping.save()

self.assertFalse(self.django_moderator.is_staff)
self.assertFalse(self.django_admin.is_staff)

self.admin_mapping.is_staff = True
self.admin_mapping.save()

self.django_admin.refresh_from_db(fields=("is_staff", ))
self.assertTrue(self.django_admin.is_staff)

# Test mapping deletion
self.admin_mapping.delete()

self.django_admin.refresh_from_db(fields=("is_staff",))
self.assertEqual(self.django_admin.groups.all().count(), 0)
self.assertFalse(self.django_admin.is_staff)

# Test mapping update
self.moderator_mapping.group = self.admin_group
Expand All @@ -388,12 +432,30 @@ def test_role_mapping_changes(self):
# Test mapping creation
new_mapping = RoleMapping.objects.create(
role=self.admin_role,
group=self.moderator_group
group=self.moderator_group,
is_staff=True
)

self.assertEqual(self.django_admin.groups.all().count(), 1)
self.assertTrue(self.moderator_group in self.django_admin.groups.all())

self.django_admin.refresh_from_db(fields=("is_staff",))
self.assertTrue(self.django_admin.is_staff)

new_mapping.delete()

# Test mapping creation (without is_staff)
new_mapping = RoleMapping.objects.create(
role=self.admin_role,
group=self.moderator_group,
)

self.assertEqual(self.django_admin.groups.all().count(), 1)
self.assertTrue(self.moderator_group in self.django_admin.groups.all())

self.django_admin.refresh_from_db(fields=("is_staff",))
self.assertFalse(self.django_admin.is_staff)

# Test that nothing happens when fixtures are loaded
pre_save.send(RoleMapping, instance=new_mapping, raw=True)

Expand Down
Loading

0 comments on commit cf68b3f

Please sign in to comment.