diff --git a/README b/README index 7e3b8c4e5..6e82d776b 100644 --- a/README +++ b/README @@ -73,3 +73,48 @@ fc_data_dic = { [FD_name, data], ], } + +Attribute mapping +================= + +You can map France Connect attributes to Authentic2 attributes through the +setting A2_FC_USER_INFO_MAPPINGS. A2_FC_USER_INFO_MAPPINGS is a dictionnary +whose keys are authentic2's attribute names and value can be France Connect +attribute names or dictionnary with the following keys: + +- `value` : a static value which will be assigned to the authentic2 attribute, + can be any Python value, +- `ref` : the name of a France Connect attribute, +- `translation` : a transformation name among: + - @insee-communes@ : translate the value using mapping from INSEE code of + communes to their name, + - @insee-countries@ : translate the value using mapping from INSEE code of + countries to their name, + - @simple@ : lookup the value using the dictionnary in @translation_simple@. +- `compute`: compute a value using a known function, only known function for now + is @today@ which returns @datetime.date.today()@. +- `verified`: set the verified flag on the value. + +Exemple: + +A2_FC_USER_INFO_MAPPINGS = { + 'first_name': 'given_name', + 'last_name': 'family_name', + 'birthdate': { 'ref': 'birthdate', 'translation': 'isodate' }, + 'birthplace': { 'ref': 'birthplace', 'translation': 'insee-communes' }, + 'birthcountry': { 'ref': 'birthcountry', 'translation': 'insee-countries' }, + 'birthplace_insee': 'birthplace', + 'birthcountry_insee': 'birthcountry', + 'title': { + 'ref': 'gender', + 'translation': 'simple', + 'translation_simple': { + 'male': 'Monsieur', + 'female': 'Madame', + } + }, + 'gender': 'gender', + 'validated': { 'value': True }, + 'validation_date': { 'compute': 'today' }, + 'validation_context': { 'value': 'France Connect' }, +} diff --git a/src/authentic2_auth_fc/app_settings.py b/src/authentic2_auth_fc/app_settings.py index 58cb91bd8..3f3d91678 100644 --- a/src/authentic2_auth_fc/app_settings.py +++ b/src/authentic2_auth_fc/app_settings.py @@ -58,9 +58,19 @@ class AppSettings(object): @property def attributes_mapping(self): return self._setting('ATTRIBUTES_MAPPING', - {'family_name': 'last_name', - 'given_name': 'first_name', - 'email': 'email'}) + { + 'family_name': 'last_name', + 'given_name': 'first_name', + 'email': 'email' + }) + + @property + def user_info_mappings(self): + return self._setting('USER_INFO_MAPPINGS', { + 'last_name': 'family_name', + 'first_name': 'given_name', + 'email': 'email', + }) @property def next_field_name(self): diff --git a/src/authentic2_auth_fc/backends.py b/src/authentic2_auth_fc/backends.py index 127f0169a..4b51ae322 100644 --- a/src/authentic2_auth_fc/backends.py +++ b/src/authentic2_auth_fc/backends.py @@ -1,42 +1,48 @@ import json import logging -from . import models, app_settings - from django.contrib.auth import get_user_model from django.contrib.auth.backends import ModelBackend +from . import models, app_settings, utils + logger = logging.getLogger(__name__) + class FcBackend(ModelBackend): def authenticate(self, sub=None, **kwargs): + user_info = kwargs.get('user_info') + user = None try: fc_account = models.FcAccount.objects.get(sub=sub, user__is_active=True) msg = 'existing user {} using sub {}'.format(fc_account.user, sub) logger.debug(msg) - return fc_account.user + user = fc_account.user except models.FcAccount.DoesNotExist: logger.debug('user with the sub {} not existing.'.format(sub)) - if app_settings.create and 'user_info' in kwargs: - User = get_user_model() - user_info = kwargs['user_info'] - user = User.objects.create( - first_name=user_info['given_name'], - last_name=user_info['family_name'], - ) - fc_account = models.FcAccount.objects.create( - user=user, - sub=sub, - token=json.dumps(kwargs['token'])) - msg = 'user creation enabled ' \ - '(given_name : {} - family_name : {}) ' \ - 'with fc_account (sub : {} - token : {})'.format( - user_info['given_name'], - user_info['family_name'], + if user_info: + if not user and app_settings.create: + User = get_user_model() + user = User.objects.create() + fc_account = models.FcAccount.objects.create( + user=user, + sub=sub, + token=json.dumps(kwargs['token'])) + msg = 'user creation enabled with fc_account (sub : {} - token : {})'.format( sub, json.dumps(kwargs['token']) ) + logger.debug(msg) + if not user: + return None + msg = 'updated (given_name : {} - family_name : {}) '.format( + user_info['given_name'], + user_info['family_name'], + ) + user.first_name = user_info['given_name'] + user.last_name = user_info['family_name'] logger.debug(msg) + utils.apply_user_info_mappings(user, user_info) return user def get_saml2_authn_context(self): diff --git a/src/authentic2_auth_fc/utils.py b/src/authentic2_auth_fc/utils.py index 10927aa57..31d1279f7 100644 --- a/src/authentic2_auth_fc/utils.py +++ b/src/authentic2_auth_fc/utils.py @@ -1,4 +1,8 @@ import urllib +import logging +import os +import json +import datetime from django.core.urlresolvers import reverse @@ -37,3 +41,82 @@ def get_mapped_attributes_flat(request): if fc_name in request.session['fc-user_info']: values[local_name] = request.session['fc-user_info'][fc_name] return values + + +def mapping_to_value(mapping, user_info): + if isinstance(mapping, basestring): + value = user_info[mapping] + elif 'ref' in mapping: + value = user_info[mapping['ref']] + elif 'value' in mapping: + value = mapping['value'] + elif 'compute' in mapping: + if mapping['compute'] == 'today': + value = datetime.date.today() + else: + raise NotImplementedError + + if 'translation' in mapping: + if mapping['translation'] == 'insee-communes': + value = resolve_insee_commune(value) + elif mapping['translation'] == 'insee-countries': + value = resolve_insee_country(value) + elif mapping['translation'] == 'isodate': + value = datetime.datetime.strptime(value, '%Y-%m-%d').date() + elif mapping['translation'] == 'simple': + value = mapping['translation_simple'].get( + value, mapping.get('translation_simple_default', '')) + else: + raise NotImplementedError + return value + + +_insee_communes = None + + +def resolve_insee_commune(insee_code): + global _insee_communes + if not _insee_communes: + _insee_communes = json.load( + open( + os.path.join( + os.path.dirname(__file__), 'insee-communes.json'))) + return _insee_communes.get(insee_code, 'Code insee inconnu') + + +_insee_countries = None + + +def resolve_insee_country(insee_code): + global _insee_countries + + if not _insee_countries: + _insee_countries = json.load( + open( + os.path.join( + os.path.dirname(__file__), 'insee-countries.json'))) + return _insee_countries.get(insee_code, 'Code insee inconnu') + + +def apply_user_info_mappings(user, user_info): + assert user + assert user_info + + logger = logging.getLogger(__name__) + mappings = app_settings.user_info_mappings + + for attribute, mapping in mappings.iteritems(): + try: + value = mapping_to_value(mapping, user_info) + except (ValueError, KeyError, NotImplementedError) as e: + logger.warning(u'auth_fc: cannot apply mapping %s <- %r: %s', attribute, mapping, e) + continue + if hasattr(user.attributes, attribute): + if getattr(mapping, 'verified', False): + setattr(user.verified_attributes, attribute, value) + else: + setattr(user.attributes, attribute, value) + elif hasattr(user, 'attribute'): + setattr(user, attribute, value) + else: + logger.warning(u'auth_fc: unknown attribut in user_info mapping: %s', attribute) diff --git a/src/authentic2_auth_fc/views.py b/src/authentic2_auth_fc/views.py index 7878f5db2..e61ee88b9 100644 --- a/src/authentic2_auth_fc/views.py +++ b/src/authentic2_auth_fc/views.py @@ -284,6 +284,7 @@ class LoginOrLinkView(PopupViewMixin, FcOAuthSessionViewMixin, View): def update_user_info(self): self.fc_account.user_info = json.dumps(self.user_info) self.fc_account.save() + utils.apply_user_info_mappings(self.fc_account.user, self.user_info) self.logger.debug('updating user_info %s', self.fc_account.user_info) def get(self, request, *args, **kwargs):