misc: use lock on email when creating user instances (#64485)
This commit is contained in:
parent
e555ca5a0a
commit
362b4cbc0c
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue