From 49a525436338b57953503a65fd0c09b654e160c2 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Mon, 11 Apr 2016 19:20:05 +0200 Subject: [PATCH] allow federating transient NameID using an attribute (fixes #10619) --- README | 8 ++++++++ mellon/adapters.py | 17 ++++++++++++++++- mellon/app_settings.py | 1 + tests/test_default_adapter.py | 26 ++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/README b/README index cff7a2a..29c571f 100644 --- a/README +++ b/README @@ -247,6 +247,14 @@ MELLON_VERIFY_SSL_CERTIFICATE Verify SSL certificate when doing HTTP requests, used when resolving artifacts. Default is True. +MELLON_TRANSIENT_FEDERATION_ATTRIBUTE +------------------------------------- + +Name of an attribute to use in replacement of the NameID content when the NameID +format is transient. Without it no login using a transient NameID can occur with +the default adapter. +Default is None. + Tests ===== diff --git a/mellon/adapters.py b/mellon/adapters.py index 99224a1..ab79521 100644 --- a/mellon/adapters.py +++ b/mellon/adapters.py @@ -107,7 +107,22 @@ class DefaultAdapter(object): def lookup_user(self, idp, saml_attributes): User = auth.get_user_model() - name_id = saml_attributes['name_id_content'] + transient_federation_attribute = utils.get_setting(idp, 'TRANSIENT_FEDERATION_ATTRIBUTE') + if saml_attributes['name_id_format'] == lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT: + if (transient_federation_attribute + and saml_attributes.get(transient_federation_attribute)): + name_id = saml_attributes[transient_federation_attribute] + if not isinstance(name_id, basestring): + if len(name_id) == 1: + name_id = name_id[0] + else: + self.logger.warning('more than one value for attribute %r, cannot federate', + transient_federation_attribute) + return None + else: + return None + else: + name_id = saml_attributes['name_id_content'] issuer = saml_attributes['issuer'] try: return User.objects.get(saml_identifiers__name_id=name_id, diff --git a/mellon/app_settings.py b/mellon/app_settings.py index ad0d38d..34c5342 100644 --- a/mellon/app_settings.py +++ b/mellon/app_settings.py @@ -32,6 +32,7 @@ class AppSettings(object): 'OPENED_SESSION_COOKIE_DOMAIN': None, 'ORGANIZATION': None, 'CONTACT_PERSONS': [], + 'TRANSIENT_FEDERATION_ATTRIBUTE': None, } @property diff --git a/tests/test_default_adapter.py b/tests/test_default_adapter.py index 7d497c4..13838a4 100644 --- a/tests/test_default_adapter.py +++ b/tests/test_default_adapter.py @@ -1,6 +1,7 @@ import threading import pytest import re +import lasso from django.contrib import auth from django.db import connection @@ -14,6 +15,7 @@ idp = { 'METADATA': file('tests/metadata.xml').read(), } saml_attributes = { + 'name_id_format': lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT, 'name_id_content': 'x' * 32, 'issuer': 'https://cresson.entrouvert.org/idp/saml2/metadata', 'username': ['foobar'], @@ -164,3 +166,27 @@ def test_provision_long_attribute(settings, django_user_model, caplog): assert 'to value %r ' % (u'y' * 30) in caplog.text() assert 'set field last_name' in caplog.text() assert 'set field email' in caplog.text() + + +def test_lookup_user_transient_with_email(private_settings): + private_settings.MELLON_TRANSIENT_FEDERATION_ATTRIBUTE = 'email' + User = auth.get_user_model() + adapter = DefaultAdapter() + saml_attributes2 = saml_attributes.copy() + saml_attributes2['name_id_format'] = lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT + assert User.objects.count() == 0 + user = adapter.lookup_user(idp, saml_attributes2) + assert user is not None + assert user.saml_identifiers.count() == 1 + assert user.saml_identifiers.first().name_id == saml_attributes2['email'][0] + + user2 = adapter.lookup_user(idp, saml_attributes2) + assert user.id == user2.id + + User.objects.all().delete() + assert User.objects.count() == 0 + + private_settings.MELLON_PROVISION = False + user = adapter.lookup_user(idp, saml_attributes) + assert user is None + assert User.objects.count() == 0