From 853fc14f3329bfcdb4b89e8261185319088a805a Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Wed, 7 Aug 2019 11:28:35 +0200 Subject: [PATCH] auth_saml: implement attribute provisionning after first login (#35283) Also fix bug in finish_create_user() where modified user was not saved. --- src/authentic2_auth_saml/adapters.py | 53 ++++++++++++++++++++-------- tests/test_auth_saml.py | 21 +++++++---- 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/src/authentic2_auth_saml/adapters.py b/src/authentic2_auth_saml/adapters.py index c2c1c200..6e2e1ec8 100644 --- a/src/authentic2_auth_saml/adapters.py +++ b/src/authentic2_auth_saml/adapters.py @@ -16,17 +16,26 @@ import logging -from mellon.adapters import DefaultAdapter +from mellon.adapters import DefaultAdapter, UserCreationError from mellon.utils import get_setting from authentic2 import utils +logger = logging.getLogger('authentic2.auth_saml') + class AuthenticAdapter(DefaultAdapter): def create_user(self, user_class): return user_class.objects.create() def finish_create_user(self, idp, saml_attributes, user): + self.provision_a2_attributes(user, idp, saml_attributes, do_raise=True) + + def provision(self, user, idp, saml_attributes): + super(AuthenticAdapter, self).provision(user, idp, saml_attributes) + self.provision_a2_attributes(user, idp, saml_attributes) + + def provision_a2_attributes(self, user, idp, saml_attributes, do_raise=False): '''Copy incoming SAML attributes to user attributes, A2_ATTRIBUTE_MAPPING must be a list of dictinnaries like: @@ -40,29 +49,36 @@ class AuthenticAdapter(DefaultAdapter): If an attribute is not mandatory any error is just logged, if the attribute is mandatory, login will fail. ''' - log = logging.getLogger(__name__) attribute_mapping = get_setting(idp, 'A2_ATTRIBUTE_MAPPING', []) + user_modified = False for mapping in attribute_mapping: attribute = mapping['attribute'] saml_attribute = mapping['saml_attribute'] mandatory = mapping.get('mandatory', False) - if not saml_attributes.get(saml_attribute): + logger.debug('auth_saml: trying mapping attribute from %r to %r', saml_attribute, attribute, + extra={'user': user}) + if saml_attribute not in saml_attributes: if mandatory: - log.error('mandatory saml attribute %r is missing', saml_attribute, - extra={'attributes': repr(saml_attributes)}) - raise ValueError('missing attribute') + logger.error('auth_saml: mandatory saml attribute %r is missing', saml_attribute, + extra={'attributes': repr(saml_attributes), 'user': user}) + if do_raise: + raise UserCreationError('missing saml_attribute %r' % saml_attribute) else: - continue + logger.debug('auth_saml: saml_attribute %r not found', saml_attribute, extra={'user': user}) + continue try: value = saml_attributes[saml_attribute] - self.set_user_attribute(user, attribute, value) + if self.set_user_attribute(user, attribute, value): + user_modified = True except Exception as e: - log.error(u'failed to set attribute %r from saml attribute %r with value %r: %s', - attribute, saml_attribute, value, e, - extra={'attributes': repr(saml_attributes)}) - if mandatory: - raise + logger.error(u'failed to set attribute %r from saml attribute %r with value %r: %s', + attribute, saml_attribute, value, e, + extra={'attributes': repr(saml_attributes), 'user': user}) + if mandatory and do_raise: + raise UserCreationError('could not set attribute %s' % attribute, e) + if user_modified: + user.save() def set_user_attribute(self, user, attribute, value): if isinstance(value, list): @@ -70,9 +86,16 @@ class AuthenticAdapter(DefaultAdapter): raise ValueError('too much values') value = value[0] if attribute in ('first_name', 'last_name', 'email', 'username'): - setattr(user, attribute, value) + if getattr(user, attribute) != value: + logger.info('auth_saml: attribute %r set to %r', attribute, value, extra={'user': user}) + setattr(user, attribute, value) + return True else: - setattr(user.attributes, attribute, value) + if getattr(user.attributes, attribute) != value: + logger.info('auth_saml: attribute %r set to %r', attribute, value, extra={'user': user}) + setattr(user.attributes, attribute, value) + return True + return False def auth_login(self, request, user): utils.login(request, user, 'saml') diff --git a/tests/test_auth_saml.py b/tests/test_auth_saml.py index fb5477c8..2e065094 100644 --- a/tests/test_auth_saml.py +++ b/tests/test_auth_saml.py @@ -14,15 +14,17 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import logging + import pytest +import lasso + from django.contrib.auth import get_user_model from authentic2.models import Attribute -pytestmark = pytest.mark.django_db - -def test_provision_attributes(): +def test_provision_attributes(db, caplog): from authentic2_auth_saml.adapters import AuthenticAdapter adapter = AuthenticAdapter() @@ -45,12 +47,19 @@ def test_provision_attributes(): } saml_attributes = { + u'issuer': 'https://idp.com/', + u'name_id_content': 'xxx', + u'name_id_format': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT, u'mail': u'john.doe@example.com', u'title': u'Mr.', } - adapter.finish_create_user(idp, saml_attributes, user) + user = adapter.lookup_user(idp, saml_attributes) + user.refresh_from_db() assert user.email == 'john.doe@example.com' assert user.attributes.title == 'Mr.' + user.delete() + + # on missing mandatory attribute, no user is created del saml_attributes['mail'] - with pytest.raises(ValueError): - adapter.finish_create_user(idp, saml_attributes, user) + assert adapter.lookup_user(idp, saml_attributes) is None + assert False -- 2.22.0