Projet

Général

Profil

0001-auth_fc-get-a-lock-on-the-sub-during-account-creatio.patch

Paul Marillonnet, 14 septembre 2022 10:34

Télécharger (8,1 ko)

Voir les différences:

Subject: [PATCH] auth_fc: get a lock on the sub during account creation
 (#65411)

 src/authentic2/custom_user/managers.py |  14 ++++
 src/authentic2_auth_fc/views.py        | 106 ++++++++++++++-----------
 2 files changed, 72 insertions(+), 48 deletions(-)
src/authentic2/custom_user/managers.py
174 174
            raise self.model.MultipleObjectsReturned
175 175
        return users[0]
176 176

  
177
    def filter_by_email(self, email):
178
        """
179
        Prevents unicode normalization collision attacks (see
180
        https://nvd.nist.gov/vuln/detail/CVE-2019-19844)
181
        """
182
        users = []
183
        for user in self.filter(email__iexact=email, is_active=True):
184
            if (
185
                unicodedata.normalize('NFKC', user.email).casefold()
186
                == unicodedata.normalize('NFKC', email).casefold()
187
            ):
188
                users.append(user)
189
        return users
190

  
177 191

  
178 192
class UserManager(BaseUserManager):
179 193
    def _create_user(self, username, email, password, is_staff, is_superuser, **extra_fields):
src/authentic2_auth_fc/views.py
111 111

  
112 112
    _next_url = None
113 113
    display_message_on_redirect = False
114
    fc_account = None
114 115

  
115 116
    @property
116 117
    def next_url(self):
......
333 334

  
334 335
    def link(self, request):
335 336
        '''Request an access grant code and associate it to the current user'''
336
        try:
337
            self.fc_account, created = models.FcAccount.objects.get_or_create(
338
                sub=self.sub,
339
                user=request.user,
340
                order=0,
341
                defaults={
342
                    'token': json.dumps(self.token),
343
                    'user_info': json.dumps(self.user_info),
344
                },
345
            )
337

  
338
        created = False
339
        with transaction.atomic():
340
            Lock.lock_identifier(identifier=self.sub)
341

  
346 342
            # Prevent adding a link with an FC account already linked with another user.
347
        except IntegrityError:
348
            # unique index check failed, find why.
349
            return self.uniqueness_check_failed(request)
343
            if not models.FcAccount.objects.filter(sub=self.sub).exclude(user=request.user).count():
344
                self.fc_account, created = models.FcAccount.objects.get_or_create(
345
                    sub=self.sub,
346
                    user=request.user,
347
                    order=0,
348
                    defaults={
349
                        'token': json.dumps(self.token),
350
                        'user_info': json.dumps(self.user_info),
351
                    },
352
                )
353
            else:
354
                # unique index check failed, find why.
355
                return self.uniqueness_check_failed(request)
350 356

  
351 357
        if created:
352 358
            logger.info('auth_fc: link created sub %s', self.sub)
......
355 361
            )
356 362
            hooks.call_hooks('event', name='fc-link', user=request.user, sub=self.sub, request=request)
357 363
        else:
358
            if self.fc_account.created <= request.user.last_login:
364
            if self.fc_account and self.fc_account.created <= request.user.last_login:
359 365
                utils_misc.record_authentication_event(request, 'france-connect')
360 366
        self.update_user_info(request.user, self.user_info)
361 367
        return self.redirect()
......
450 456
            user = User.objects.create(ou=get_default_ou())
451 457
            created = True
452 458

  
453
        try:
454
            if created:
455
                user.set_unusable_password()
456
                user.save()
457

  
458
            # As we intercept IntegrityError and we can never be sure if we are
459
            # in a transaction or not, we must use one to prevent later SQL
460
            # queries to fail.
461
            with transaction.atomic():
459
        if created:
460
            user.set_unusable_password()
461
            user.save()
462

  
463
        # As we intercept IntegrityError and we can never be sure if we are
464
        # in a transaction or not, we must use one to prevent later SQL
465
        # queries to fail.
466
        with transaction.atomic():
467

  
468
            Lock.lock_identifier(identifier=sub)
469

  
470
            # reshuffle existing accounts order
471
            Lock.lock_email(email=user.email)
472
            for account in models.FcAccount.objects.filter(user=user).order_by('-order'):
473
                account.order += 1
474
                account.save()
475

  
476
            if not models.FcAccount.objects.filter(sub=sub).count():
462 477
                models.FcAccount.objects.create(
463 478
                    user=user,
464 479
                    sub=sub,
......
466 481
                    token=json.dumps(token),
467 482
                    user_info=json.dumps(user_info),
468 483
                )
469
        except IntegrityError:
470
            # uniqueness check failed, as the user is new, it can only mean that the sub is not unique
471
            # let's try again
472
            if created:
473
                user.delete()
474
            return utils_misc.authenticate(request, sub=sub, token=token, user_info=user_info), False
475
        except Exception:
476
            # if anything unexpected happen and user was created, delete it and re-raise
477
            if created:
478
                user.delete()
479
            raise
484
        if created:
485
            logger.info('auth_fc: new account "%s" created with FranceConnect sub "%s"', user, sub)
486
            hooks.call_hooks('event', name='fc-create', user=user, sub=sub, request=request)
487
            # FC account creation does not rely on the registration_completion generic view.
488
            # Registration event has to be recorded here:
489
            request.journal.record('user.registration', user=user, how='france-connect')
480 490
        else:
481
            if created:
482
                logger.info('auth_fc: new account "%s" created with FranceConnect sub "%s"', user, sub)
483
                hooks.call_hooks('event', name='fc-create', user=user, sub=sub, request=request)
484
                # FC account creation does not rely on the registration_completion generic view.
485
                # Registration event has to be recorded here:
486
                request.journal.record('user.registration', user=user, how='france-connect')
487
            else:
488
                logger.info('auth_fc: existing account "%s" linked to FranceConnect sub "%s"', user, sub)
489
                hooks.call_hooks('event', name='fc-link', user=user, sub=sub, request=request)
491
            logger.info('auth_fc: existing account "%s" linked to FranceConnect sub "%s"', user, sub)
492
            hooks.call_hooks('event', name='fc-link', user=user, sub=sub, request=request)
490 493

  
491 494
        authenticated_user = utils_misc.authenticate(request, sub=sub, user_info=user_info, token=token)
492 495
        return authenticated_user, created
......
528 531
        if not a2_app_settings.A2_EMAIL_IS_UNIQUE:
529 532
            qs = qs.filter(ou=ou)
530 533

  
531
        Lock.lock_email(email)
532
        try:
533
            user = qs.get_by_email(email)
534
        except User.DoesNotExist:
535
            return User.objects.create(ou=ou, email=email), True
534
        with transaction.atomic():
535
            Lock.lock_email(email)
536

  
537
            users = qs.filter_by_email(email)
538
            if not users:
539
                return User.objects.create(ou=ou, email=email), True
540

  
541
        if len(users) > 1:
542
            # oops, something went wrong there, several users with the same
543
            # email within the same organization unit, aborting.
544
            return None, False
536 545

  
546
        user = users[0]
537 547
        if user.ou != ou:
538 548
            raise UserOutsideDefaultOu
539 549
        return user, False
540
-