Fix #1039: setting to enforce email signup verification

This commit is contained in:
Eliot Berriot 2020-04-01 14:34:56 +02:00
commit 93f2c9f83c
No known key found for this signature in database
GPG key ID: 6B501DFD73514E14
16 changed files with 365 additions and 30 deletions

View file

@ -22,3 +22,6 @@ class FunkwhaleAccountAdapter(DefaultAccountAdapter):
def send_mail(self, template_prefix, email, context):
context.update(get_email_context())
return super().send_mail(template_prefix, email, context)
def get_login_redirect_url(self, request):
return "noop"

View file

@ -1,4 +1,33 @@
from django.contrib.auth import backends, get_user_model
from allauth.account import auth_backends
from funkwhale_api.common import authentication
# ugly but allauth doesn't offer an easy way to override the querysets
# used to retrieve users, so we monkey patch
def decorate_for_auth(func):
def inner(*args, **kwargs):
qs = func(*args, **kwargs)
try:
return qs.for_auth()
except AttributeError:
return (
get_user_model()
.objects.all()
.for_auth()
.filter(pk__in=[u.pk for u in qs])
)
return inner
auth_backends.filter_users_by_email = decorate_for_auth(
auth_backends.filter_users_by_email
)
auth_backends.filter_users_by_username = decorate_for_auth(
auth_backends.filter_users_by_username
)
class ModelBackend(backends.ModelBackend):
@ -7,11 +36,17 @@ class ModelBackend(backends.ModelBackend):
Select related to avoid two additional queries
"""
try:
user = (
get_user_model()
._default_manager.select_related("actor__domain")
.get(pk=user_id)
)
user = get_user_model().objects.all().for_auth().get(pk=user_id)
except get_user_model().DoesNotExist:
return None
return user if self.user_can_authenticate(user) else None
def user_can_authenticate(self, user):
return super().user_can_authenticate(
user
) and not authentication.should_verify_email(user)
class AllAuthBackend(auth_backends.AuthenticationBackend, ModelBackend):
pass

View file

@ -31,6 +31,23 @@ class GroupFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
self.permissions.add(*perms)
@registry.register
class EmailAddressFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
verified = False
primary = True
class Meta:
model = "account.EmailAddress"
@registry.register
class EmailConfirmationFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
email_address = factory.SubFactory(EmailAddressFactory)
class Meta:
model = "account.EmailConfirmation"
@registry.register
class InvitationFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
owner = factory.LazyFunction(lambda: UserFactory())
@ -91,6 +108,16 @@ class UserFactory(factory.django.DjangoModelFactory):
self.save(update_fields=["actor"])
return self.actor
@factory.post_generation
def verified_email(self, create, extracted, **kwargs):
if not create or extracted is None:
return
return EmailConfirmationFactory(
email_address__verified=extracted,
email_address__email=self.email,
email_address__user=self,
)
@registry.register(name="users.SuperUser")
class SuperUserFactory(UserFactory):
@ -129,6 +156,7 @@ class AccessTokenFactory(factory.django.DjangoModelFactory):
user = factory.SubFactory(UserFactory)
expires = factory.Faker("future_datetime", tzinfo=pytz.UTC)
token = factory.Faker("uuid4")
scope = "read"
class Meta:
model = "users.AccessToken"

View file

@ -9,13 +9,14 @@ import string
import uuid
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.models import AbstractUser, UserManager as BaseUserManager
from django.db import models
from django.dispatch import receiver
from django.urls import reverse
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from allauth.account.models import EmailAddress
from django_auth_ldap.backend import populate_user as ldap_populate_user
from oauth2_provider import models as oauth2_models
from oauth2_provider import validators as oauth2_validators
@ -94,6 +95,26 @@ def get_default_funkwhale_support_message_display_date():
)
class UserQuerySet(models.QuerySet):
def for_auth(self):
"""Optimization to avoid additional queries during authentication"""
qs = self.select_related("actor__domain")
verified_emails = EmailAddress.objects.filter(
user=models.OuterRef("id"), primary=True
).values("verified")[:1]
subquery = models.Subquery(verified_emails)
return qs.annotate(has_verified_primary_email=subquery)
class UserManager(BaseUserManager):
def get_queryset(self):
return UserQuerySet(self.model, using=self._db)
def get_by_natural_key(self, key):
obj = BaseUserManager.get_by_natural_key(self.all().for_auth(), key)
return obj
class User(AbstractUser):
# First Name and Last Name do not cover name patterns
@ -169,6 +190,8 @@ class User(AbstractUser):
blank=True,
)
objects = UserManager()
def __str__(self):
return self.username

View file

@ -1,11 +1,24 @@
import urllib.parse
import oauthlib.oauth2
from funkwhale_api.common import authentication
def check(request):
user = request.user
request.user = user.__class__.objects.all().for_auth().get(pk=user.pk)
if authentication.should_verify_email(request.user):
setattr(request, "oauth2_error", {"error": "unverified_email"})
return False
return True
class OAuth2Server(oauthlib.oauth2.Server):
def verify_request(self, uri, *args, **kwargs):
valid, request = super().verify_request(uri, *args, **kwargs)
if valid:
if not check(request):
return False, request
return valid, request
# maybe the token was given in the querystring?
@ -21,5 +34,8 @@ class OAuth2Server(oauthlib.oauth2.Server):
valid = self.request_validator.validate_bearer_token(
token, request.scopes, request
)
if valid:
if not check(request):
return False, request
return valid, request