0001-auth_fc-make-user-and-sub-relatively-unique-19959.patch
src/authentic2_auth_fc/backends.py | ||
---|---|---|
19 | 19 | |
20 | 20 |
from django.contrib.auth import get_user_model |
21 | 21 |
from django.contrib.auth.backends import ModelBackend |
22 |
from django.core.exceptions import PermissionDenied, MultipleObjectsReturned |
|
23 |
from django.db import IntegrityError |
|
22 | 24 | |
23 | 25 |
from authentic2.a2_rbac.utils import get_default_ou |
24 | 26 |
from authentic2 import hooks |
... | ... | |
33 | 35 |
user_info = kwargs.get('user_info') |
34 | 36 |
user = None |
35 | 37 |
try: |
36 |
fc_account = models.FcAccount.objects.get(sub=sub, user__is_active=True) |
|
37 |
logger.debug(u'existing user %s using sub %s', fc_account.user, sub) |
|
38 |
user = fc_account.user |
|
38 |
try: |
|
39 |
account = models.FcAccount.objects.select_related().get(sub=sub) |
|
40 |
except MultipleObjectsReturned: |
|
41 |
account = models.FcAccount.objects.select_related().get(sub=sub, order=0) |
|
39 | 42 |
except models.FcAccount.DoesNotExist: |
40 | 43 |
logger.debug(u'user with the sub %s does not exist.', sub) |
44 |
else: |
|
45 |
user = fc_account.user |
|
46 |
logger.debug(u'found user %s with sub %s', user, sub) |
|
47 |
if not user.is_active: |
|
48 |
logger.info(u'user %s login refused, it is inactive', user) |
|
49 |
raise PermissionDenied |
|
41 | 50 |
if user_info: |
42 | 51 |
if not user and app_settings.create: |
43 | 52 |
User = get_user_model() |
44 | 53 |
user = User.objects.create(ou=get_default_ou()) |
45 |
fc_account = models.FcAccount.objects.create( |
|
46 |
user=user, |
|
47 |
sub=sub, |
|
48 |
token=json.dumps(kwargs['token'])) |
|
49 |
logger.debug(u'user creation enabled with fc_account (sub : %s - token : %s)', |
|
50 |
sub, json.dumps(kwargs['token'])) |
|
51 |
hooks.call_hooks('event', name='fc-create', user=user, sub=sub) |
|
54 |
try: |
|
55 |
fc_account = models.FcAccount.objects.create( |
|
56 |
user=user, |
|
57 |
sub=sub, |
|
58 |
order=0, |
|
59 |
token=json.dumps(kwargs['token'])) |
|
60 |
except IntegrityError: |
|
61 |
# uniqueness check failed, as the user is new, it can only means that the sub is not unique |
|
62 |
# let's try again |
|
63 |
return self.authenticate(sub, **kwargs) |
|
64 |
else: |
|
65 |
logger.debug(u'user creation enabled with fc_account (sub : %s - token : %s)', |
|
66 |
sub, json.dumps(kwargs['token'])) |
|
67 |
hooks.call_hooks('event', name='fc-create', user=user, sub=sub) |
|
68 | ||
52 | 69 |
if not user: |
53 | 70 |
return None |
71 | ||
54 | 72 |
logger.debug(u'updated (given_name : %s - family_name : %s)', user_info['given_name'], |
55 | 73 |
user_info['family_name']) |
56 | 74 |
user.first_name = user_info['given_name'] |
src/authentic2_auth_fc/migrations/0002_fcaccount_order1.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# Generated by Django 1.11.20 on 2019-06-12 21:27 |
|
3 |
from __future__ import unicode_literals |
|
4 | ||
5 |
from collections import Counter |
|
6 | ||
7 |
from django.db import migrations, models |
|
8 | ||
9 | ||
10 |
def set_fcaccount_order(apps, schema_editor): |
|
11 |
FcAccount = apps.get_model('authentic2_auth_fc', 'FcAccount') |
|
12 |
c = Counter() |
|
13 |
FcAccount.objects.update(order=0) |
|
14 |
user_ids = ( |
|
15 |
FcAccount.objects |
|
16 |
.values('user_id') |
|
17 |
.annotate(total=models.Count('id')) |
|
18 |
.filter(total__gt=1) |
|
19 |
.values_list('user_id', flat=True) |
|
20 |
) |
|
21 |
subs = ( |
|
22 |
FcAccount.objects |
|
23 |
.values_list('sub') |
|
24 |
.annotate(total=models.Count('id')) |
|
25 |
.filter(total__gt=1) |
|
26 |
.values_list('sub', flat=True) |
|
27 |
) |
|
28 | ||
29 |
for account in FcAccount.objects.filter(models.Q(user_id__in=user_ids) | models.Q(sub__in=subs)): |
|
30 |
order = max(c[account.user_id], c[account.sub]) |
|
31 |
if account.order != order: |
|
32 |
account.order = order |
|
33 |
account.save() |
|
34 |
c[account.user_id] = order + 1 |
|
35 |
c[account.sub] = order + 1 |
|
36 |
assert FcAccount.objects.filter(order__isnull=True).count() == 0 |
|
37 | ||
38 | ||
39 |
def noop(apps, schema_editor): |
|
40 |
pass |
|
41 | ||
42 | ||
43 |
class Migration(migrations.Migration): |
|
44 |
# Disembiguate sub and user_id columns using an integer order column, then |
|
45 |
# force sub/order and user_id/order pairs unique to be unique in order |
|
46 |
# to move to a model where user and sub are unique without breaking on existing data |
|
47 | ||
48 |
dependencies = [ |
|
49 |
('authentic2_auth_fc', '0001_initial'), |
|
50 |
] |
|
51 | ||
52 |
operations = [ |
|
53 |
migrations.AddField( |
|
54 |
model_name='fcaccount', |
|
55 |
name='order', |
|
56 |
field=models.IntegerField(null=True, verbose_name='order'), |
|
57 |
), |
|
58 |
migrations.RunPython(set_fcaccount_order, noop), |
|
59 |
] |
src/authentic2_auth_fc/migrations/0003_fcaccount_order2.py | ||
---|---|---|
1 |
# -*- coding: utf-8 -*- |
|
2 |
# Generated by Django 1.11.20 on 2019-06-13 09:44 |
|
3 |
from __future__ import unicode_literals |
|
4 | ||
5 |
from django.conf import settings |
|
6 |
from django.db import migrations, models |
|
7 | ||
8 | ||
9 |
class Migration(migrations.Migration): |
|
10 | ||
11 |
dependencies = [ |
|
12 |
migrations.swappable_dependency(settings.AUTH_USER_MODEL), |
|
13 |
('authentic2_auth_fc', '0002_fcaccount_order1'), |
|
14 |
] |
|
15 | ||
16 |
operations = [ |
|
17 |
migrations.AlterField( |
|
18 |
model_name='fcaccount', |
|
19 |
name='order', |
|
20 |
field=models.PositiveIntegerField(default=0, verbose_name='order'), |
|
21 |
), |
|
22 |
migrations.AlterUniqueTogether( |
|
23 |
name='fcaccount', |
|
24 |
unique_together=set([('sub', 'order'), ('user', 'order')]), |
|
25 |
), |
|
26 |
] |
src/authentic2_auth_fc/models.py | ||
---|---|---|
87 | 87 |
sub = models.TextField( |
88 | 88 |
verbose_name=_('sub'), |
89 | 89 |
db_index=True) |
90 |
order = models.PositiveIntegerField( |
|
91 |
verbose_name=_('order'), |
|
92 |
default=0) |
|
90 | 93 |
token = models.TextField(verbose_name=_('access token')) |
91 | 94 |
user_info = models.TextField(verbose_name=_('access token'), blank=True, null=True) |
92 | 95 | |
... | ... | |
108 | 111 |
if 'family_name' in user_info: |
109 | 112 |
display_name.append(user_info['family_name']) |
110 | 113 |
return ' '.join(display_name) |
114 | ||
115 |
class Meta: |
|
116 |
unique_together = [ |
|
117 |
('sub', 'order'), |
|
118 |
('user', 'order'), |
|
119 |
] |
src/authentic2_auth_fc/views.py | ||
---|---|---|
24 | 24 | |
25 | 25 | |
26 | 26 |
import django |
27 |
from django.db import IntegrityError |
|
27 | 28 |
from django.views.generic import View, FormView |
28 | 29 |
from django.http import HttpResponseRedirect, Http404 |
29 | 30 |
from django.contrib.auth import REDIRECT_FIELD_NAME, get_user_model |
... | ... | |
162 | 163 |
redirect_field_name = REDIRECT_FIELD_NAME |
163 | 164 |
in_popup = False |
164 | 165 |
token = None |
166 |
user_info = None |
|
165 | 167 | |
166 | 168 |
def get_in_popup(self): |
167 | 169 |
return self.in_popup |
... | ... | |
331 | 333 |
self.scopes.extend(scopes) |
332 | 334 |
return ask_authorization(request, self.get_scopes(), self.logger) |
333 | 335 | |
336 |
@property |
|
337 |
def fc_display_name(self): |
|
338 |
'''Human representation of the current FC account''' |
|
339 |
if not self.user_info: |
|
340 |
return u'' |
|
341 |
return u'{0} {1}'.format(self.user_info['family_name'], self.user_info['given_name']) |
|
342 | ||
334 | 343 | |
335 | 344 |
class PopupViewMixin(object): |
336 | 345 |
def get_in_popup(self): |
... | ... | |
344 | 353 |
''' |
345 | 354 | |
346 | 355 |
def update_user_info(self): |
356 |
self.fc_account.token = json.dumps(self.token) |
|
347 | 357 |
self.fc_account.user_info = json.dumps(self.user_info) |
348 |
self.fc_account.save() |
|
358 |
self.fc_account.save(update_fields=['token', 'user_info'])
|
|
349 | 359 |
utils.apply_user_info_mappings(self.fc_account.user, self.user_info) |
350 | 360 |
self.logger.debug('updating user_info %s', self.fc_account.user_info) |
351 | 361 | |
362 |
def uniqueness_check_failed(self, request): |
|
363 |
if models.FcAccount.objects.filter(user=request.user, order=0).count(): |
|
364 |
messages.error(request, |
|
365 |
_('Your account is already linked to a FranceConnect account')) |
|
366 |
else: |
|
367 |
messages.error(request, |
|
368 |
_('The FranceConnect account {} is already' |
|
369 |
' linked with another account.').format(self.fc_display_name)) |
|
370 |
return self.redirect(request) |
|
371 | ||
352 | 372 |
def get(self, request, *args, **kwargs): |
353 | 373 |
registration = True if 'registration' in request.GET else False |
354 | 374 |
'''Request an access grant code and associate it to the current user''' |
355 | 375 |
self.service_slug = request.GET.get(constants.SERVICE_FIELD_NAME) |
356 | 376 |
if request.user.is_authenticated(): |
357 |
# Prevent to add a link with an FC account already linked with another user. |
|
358 | 377 |
try: |
359 |
fc_account = models.FcAccount.objects.get(sub=self.sub, user__is_active=True) |
|
360 |
if fc_account.user is not request.user: |
|
361 |
msg = 'Attempt to link FC account {} already linked with user {}' |
|
362 |
self.logger.info(msg.format(self.sub, fc_account.user)) |
|
363 |
messages.error(request, |
|
364 |
_('The FranceConnect account {} is already' |
|
365 |
' linked with another account.').format(fc_account)) |
|
366 |
return self.redirect(request) |
|
367 |
except models.FcAccount.DoesNotExist: |
|
368 |
pass |
|
369 | ||
370 |
# Prevent to add a link to an user which is already linked to an FC account |
|
371 |
if request.user.fc_accounts.exists(): |
|
372 |
self.logger.warning(u'cannot link to sub %s, account is already linked to an ' |
|
373 |
u'FC account', self.sub) |
|
374 |
messages.error(request, |
|
375 |
_('Your account is already linked to a FranceConnect account')) |
|
376 |
return self.redirect(request) |
|
378 |
self.fc_account, created = models.FcAccount.objects.get_or_create( |
|
379 |
sub=self.sub, user=request.user, order=0, |
|
380 |
defaults={'token': json.dumps(self.token)}) |
|
381 |
except IntegrityError: |
|
382 |
# unique index check failed, find why. |
|
383 |
return self.uniqueness_check_failed(request) |
|
377 | 384 | |
378 |
json_token = json.dumps(self.token) |
|
379 |
self.fc_account, created = models.FcAccount.objects.get_or_create( |
|
380 |
defaults={'token': json_token}, |
|
381 |
sub=self.sub, user=request.user) |
|
382 | 385 |
if created: |
383 | 386 |
self.logger.info('fc link created sub %s', self.sub) |
384 |
self.update_user_info() |
|
385 |
data = utils.get_mapped_attributes_flat(request) |
|
386 |
if 'email' in data: |
|
387 |
messages.info(request, |
|
388 |
_('Your FranceConnect account {} with ' |
|
389 |
'email {} has been linked.').format(self.fc_account, |
|
390 |
data['email'])) |
|
391 |
else: |
|
392 |
messages.info(request, _('Your FranceConnect account {} ' |
|
393 |
'has been linked.').format(self.fc_account)) |
|
394 |
hooks.call_hooks('event', name='fc-link', user=request.user, sub=self.sub, |
|
395 |
request=request) |
|
387 |
messages.info(request, |
|
388 |
_('Your FranceConnect account {} has been linked.').format(self.fc_display_name)) |
|
389 |
hooks.call_hooks('event', name='fc-link', user=request.user, sub=self.sub, request=request) |
|
396 | 390 |
else: |
397 |
self.fc_account.token = json_token |
|
398 |
self.fc_account.save() |
|
399 |
self.update_user_info() |
|
400 | 391 |
messages.info(request, _('Your local account has been updated.')) |
392 |
self.update_user_info() |
|
401 | 393 |
return self.redirect(request) |
402 | 394 | |
403 | 395 |
default_ou = get_default_ou() |
... | ... | |
407 | 399 |
sub=self.sub, |
408 | 400 |
user_info=self.user_info, |
409 | 401 |
token=self.token) |
402 |
if user: |
|
403 |
self.fc_account = user.fc_accounts.get(order=0) |
|
410 | 404 |
if not user and self.user_info.get('email') and email_is_unique: |
411 | 405 |
email = self.user_info['email'] |
412 | 406 |
User = get_user_model() |
... | ... | |
424 | 418 |
user = qs[0] |
425 | 419 |
# but does he have already a link to an FC account ? |
426 | 420 |
if not user.fc_accounts.exists(): |
427 |
fc_account, created = models.FcAccount.objects.get_or_create( |
|
428 |
defaults={'token': json.dumps(self.token)}, |
|
429 |
sub=self.sub, user=user) |
|
421 |
try: |
|
422 |
self.fc_account, created = models.FcAccount.objects.get_or_create( |
|
423 |
defaults={'token': json.dumps(self.token)}, |
|
424 |
sub=self.sub, user=user, order=0) |
|
425 |
except IntegrityError: |
|
426 |
# unique index check failed, find why. |
|
427 |
return self.uniqueness_check_failed(request) |
|
428 | ||
430 | 429 |
if created: |
431 | 430 |
self.logger.info(u'fc link created sub %s user %s', self.sub, user) |
432 | 431 |
hooks.call_hooks('event', name='fc-link', user=user, sub=self.sub, |
... | ... | |
446 | 445 |
return self.redirect(request) |
447 | 446 |
if user: |
448 | 447 |
a2_utils.login(request, user, 'france-connect', service_slug=self.service_slug) |
449 |
self.fc_account = models.FcAccount.objects.get(sub=self.sub, user=user) |
|
450 |
self.fc_account.token = json.dumps(self.token) |
|
451 |
self.fc_account.save(update_fields=['token']) |
|
452 | 448 |
self.update_user_info() |
453 | 449 |
self.logger.info('logged in using fc sub %s', self.sub) |
454 | 450 |
return self.redirect(request) |
455 |
- |