misc: use lock on email when creating user instances (#64485)

This commit is contained in:
Benjamin Dauvergne 2022-04-07 23:37:17 +02:00
parent e555ca5a0a
commit 362b4cbc0c
6 changed files with 70 additions and 35 deletions

View File

@ -52,7 +52,7 @@ from authentic2.backends import is_user_authenticable
from authentic2.compat_lasso import lasso
from authentic2.ldap_utils import FilterFormatter
from authentic2.middleware import StoreRequestMiddleware
from authentic2.models import UserExternalId
from authentic2.models import Lock, UserExternalId
from authentic2.user_login_failure import user_login_failure, user_login_success
from authentic2.utils import crypto
from authentic2.utils.misc import PasswordChangeError, to_list
@ -1373,6 +1373,7 @@ class LDAPBackend:
if email_field not in attributes:
return
email = attributes[email_field][0]
Lock.lock_email(email)
try:
log.debug('ldap: lookup using email %r', email)
return self._lookup_user_queryset(block=block).get(ou=ou, email=email)

View File

@ -596,6 +596,10 @@ class Lock(models.Model):
# recoverable (i.e. the transaction can continue after)
raise cls.Error
@classmethod
def lock_email(cls, email, nowait=False):
cls.lock('email:%s' % email, nowait=nowait)
class Meta:
verbose_name = _('Lock')
verbose_name_plural = _('Lock')

View File

@ -29,6 +29,7 @@ from django.contrib.auth.decorators import login_required
from django.contrib.auth.views import PasswordChangeView as DjPasswordChangeView
from django.core.exceptions import FieldDoesNotExist, ValidationError
from django.db.models.query import Q
from django.db.transaction import atomic
from django.forms import CharField
from django.http import (
Http404,
@ -59,6 +60,7 @@ from .a2_rbac.utils import get_default_ou
from .forms import passwords as passwords_forms
from .forms import profile as profile_forms
from .forms import registration as registration_forms
from .models import Lock
from .utils import crypto
from .utils import misc as utils_misc
from .utils import switch_user as utils_switch_user
@ -1094,6 +1096,7 @@ class RegistrationCompletionView(CreateView):
url = utils_misc.make_url(self.success_url)
return url
@atomic(savepoint=False)
def dispatch(self, request, *args, **kwargs):
registration_token = kwargs['registration_token'].replace(' ', '')
try:
@ -1112,6 +1115,7 @@ class RegistrationCompletionView(CreateView):
self.authentication_method = self.token.get('authentication_method', 'email')
self.email = self.token['email']
Lock.lock_email(self.email)
if 'ou' in self.token:
self.ou = OU.objects.get(pk=self.token['ou'])
else:

View File

@ -35,13 +35,12 @@ from requests_oauthlib import OAuth2Session
from authentic2 import app_settings as a2_app_settings
from authentic2 import constants, hooks
from authentic2 import models as a2_models
from authentic2.a2_rbac.utils import get_default_ou
from authentic2.forms.passwords import SetPasswordForm
from authentic2.models import Attribute, Lock
from authentic2.utils import misc as utils_misc
from authentic2.utils import views as utils_views
from authentic2.utils.crypto import check_hmac_url, hash_chain, hmac_url
from authentic2.utils.models import safe_get_or_create
from . import app_settings, models
from .utils import (
@ -313,6 +312,7 @@ class LoginOrLinkView(View):
self.update_user_info(request.user, self.user_info)
return self.redirect()
@transaction.atomic
def login(self, request):
user = utils_misc.authenticate(request, sub=self.sub, user_info=self.user_info, token=self.token)
@ -354,7 +354,7 @@ class LoginOrLinkView(View):
def missing_required_attributes(self, user):
'''Compute if user has not filled some required attributes.'''
name_to_label = dict(
a2_models.Attribute.objects.filter(required=True, user_editable=True).values_list('name', 'label')
Attribute.objects.filter(required=True, user_editable=True).values_list('name', 'label')
)
required = list(a2_app_settings.A2_REGISTRATION_REQUIRED_FIELDS) + list(name_to_label)
missing = []
@ -399,7 +399,7 @@ class LoginOrLinkView(View):
% {'email': email, 'user': user.get_full_name()},
)
else: # no email, we cannot disembiguate users, let's create it anyway
user = User.objects.create()
user = User.objects.create(ou=get_default_ou())
created = True
try:
@ -476,18 +476,19 @@ class LoginOrLinkView(View):
def get_or_create_user_with_email(self, email):
ou = get_default_ou()
if a2_app_settings.A2_EMAIL_IS_UNIQUE:
instance, created = safe_get_or_create(
User, email__iexact=email, defaults={'email': email, 'ou': ou}
)
if instance.ou != ou:
assert not created # should not be possible
raise UserOutsideDefaultOu
return instance, created
elif ou.email_is_unique:
return safe_get_or_create(User, ou=ou, email__iexact=email, defaults={'email': email, 'ou': ou})
else:
return User.objects.create(email=email), True
qs = User.objects.filter(email__iexact=email)
if not a2_app_settings.A2_EMAIL_IS_UNIQUE:
qs = qs.filter(ou=ou)
Lock.lock_email(email)
try:
user = qs.get()
except User.DoesNotExist:
return User.objects.create(ou=ou, email=email), True
if user.ou != ou:
raise UserOutsideDefaultOu
return user, False
login_or_link = LoginOrLinkView.as_view()

View File

@ -20,12 +20,14 @@ import logging
import requests
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
from django.db.transaction import atomic
from django.utils.timezone import now
from jwcrypto.jwk import JWK
from jwcrypto.jwt import JWT
from authentic2 import app_settings, hooks
from authentic2.a2_rbac.models import OrganizationalUnit
from authentic2.models import Lock
from authentic2.utils.crypto import base64url_encode
from authentic2.utils.template import Template
@ -35,6 +37,12 @@ from . import models, utils
class OIDCBackend(ModelBackend):
# pylint: disable=arguments-renamed
def authenticate(self, request, access_token=None, id_token=None, nonce=None, provider=None):
with atomic(savepoint=False):
return self._authenticate(
request, access_token=access_token, id_token=id_token, nonce=nonce, provider=provider
)
def _authenticate(self, request, access_token=None, id_token=None, nonce=None, provider=None):
logger = logging.getLogger(__name__)
if None in (id_token, provider):
return
@ -254,25 +262,27 @@ class OIDCBackend(ModelBackend):
linked = False
if not user:
if provider.strategy == models.OIDCProvider.STRATEGY_CREATE:
try:
if app_settings.A2_EMAIL_IS_UNIQUE and email:
user = User.objects.get(email__iexact=email)
elif provider.ou and provider.ou.email_is_unique:
user = User.objects.get(ou=provider.ou, email__iexact=email)
linked = True
except User.DoesNotExist:
pass
except User.MultipleObjectsReturned:
logger.error(
'auth_oidc: cannot create user with sub "%s", too many users with the same email "%s"'
' in ou "%s"',
id_token.sub,
email,
provider.ou,
)
return
if email:
Lock.lock_email(email)
try:
if app_settings.A2_EMAIL_IS_UNIQUE and email:
user = User.objects.get(email__iexact=email)
elif provider.ou and provider.ou.email_is_unique:
user = User.objects.get(ou=provider.ou, email__iexact=email)
linked = True
except User.DoesNotExist:
pass
except User.MultipleObjectsReturned:
logger.error(
'auth_oidc: cannot create user with sub "%s", too many users with the same email "%s"'
' in ou "%s"',
id_token.sub,
email,
provider.ou,
)
return
if not user:
user = User.objects.create(ou=provider.ou)
user = User.objects.create(ou=provider.ou, email=email or '')
user.set_unusable_password()
created_user = True
oidc_account, created = models.OIDCAccount.objects.get_or_create(

View File

@ -28,6 +28,7 @@ from authentic2.a2_rbac.models import OrganizationalUnit as OU
from authentic2.a2_rbac.models import Role
from authentic2.a2_rbac.utils import get_default_ou
from authentic2.backends import get_user_queryset
from authentic2.models import Lock
from authentic2.utils import misc as utils_misc
from authentic2.utils.evaluate import evaluate_condition
@ -72,6 +73,20 @@ class AuthenticAdapter(DefaultAdapter):
user.save()
return user
@atomic(savepoint=False)
def lookup_user(self, idp, saml_attributes, *args, **kwargs):
return super().lookup_user(idp, saml_attributes, *args, **kwargs)
def _lookup_by_attributes(self, idp, saml_attributes, lookup_by_attributes):
for rule in lookup_by_attributes:
user_field = rule.get('user_field')
saml_attribute = rule.get('saml_attribute')
emails = saml_attributes.get(saml_attribute)
if user_field and user_field == 'email' and emails:
for email in emails:
Lock.lock_email(email)
return super()._lookup_by_attributes(idp, saml_attributes, lookup_by_attributes)
def finish_create_user(self, idp, saml_attributes, user):
try:
self.provision_a2_attributes(user, idp, saml_attributes)