From 136fe60140160f8e1c18974fce5dc3551fa70bd6 Mon Sep 17 00:00:00 2001 From: Patrick Uiterwijk Date: Feb 15 2016 22:02:08 +0000 Subject: Use mapped attributes for nameid detection This moves up the attribute mapping to before finding the NameID, which allows remapping of username or email fields used for NameID with the default attribute mapping schemes. Ticket: #208 Signed-off-by: Patrick Uiterwijk Reviewed-by: Rob Crittenden --- diff --git a/ipsilon/providers/saml2/auth.py b/ipsilon/providers/saml2/auth.py index 3a6c790..08423a6 100644 --- a/ipsilon/providers/saml2/auth.py +++ b/ipsilon/providers/saml2/auth.py @@ -228,6 +228,32 @@ class AuthenticateRequest(ProviderPageBase): authtime_notbefore = authtime - skew authtime_notafter = authtime + skew + # Let's first do the attribute mapping, so we could map the username + # Check attribute policy and perform mapping and filtering. + # If the SP has its own mapping or filtering policy use that + # instead of the global policy. + if (provider.attribute_mappings is not None and + len(provider.attribute_mappings) > 0): + attribute_mappings = provider.attribute_mappings + else: + attribute_mappings = self.cfg.default_attribute_mapping + if (provider.allowed_attributes is not None and + len(provider.allowed_attributes) > 0): + allowed_attributes = provider.allowed_attributes + else: + allowed_attributes = self.cfg.default_allowed_attributes + self.debug("Allowed attrs: %s" % allowed_attributes) + self.debug("Mapping: %s" % attribute_mappings) + policy = Policy(attribute_mappings, allowed_attributes) + userattrs = us.get_user_attrs() + mappedattrs, _ = policy.map_attributes(userattrs) + attributes = policy.filter_attributes(mappedattrs) + + if '_groups' in attributes and 'groups' not in attributes: + attributes['groups'] = attributes['_groups'] + + self.debug("%s's attributes: %s" % (user.name, attributes)) + # TODO: get authentication type fnd name format from session # need to save which login manager authenticated and map it to a # saml2 authentication context @@ -250,19 +276,18 @@ class AuthenticateRequest(ProviderPageBase): value = hashlib.sha512() value.update(idpsalt) value.update(login.remoteProviderId) - value.update(user.name) + value.update(mappedattrs.get('_username')) nameid = '_' + value.hexdigest() elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT: nameid = '_' + uuid.uuid4().hex elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS: - userattrs = us.get_user_attrs() nameid = userattrs.get('gssapi_principal_name') elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL: - nameid = us.get_user().email + nameid = mappedattrs.get('email') if not nameid: nameid = '%s@%s' % (user.name, self.cfg.default_email_domain) elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED: - nameid = provider.normalize_username(user.name) + nameid = provider.normalize_username(mappedattrs.get('_username')) if nameid: login.assertion.subject.nameId.format = nameidfmt @@ -274,31 +299,6 @@ class AuthenticateRequest(ProviderPageBase): raise AuthenticationError("Unavailable Name ID type", lasso.SAML2_STATUS_CODE_AUTHN_FAILED) - # Check attribute policy and perform mapping and filtering. - # If the SP has its own mapping or filtering policy use that - # instead of the global policy. - if (provider.attribute_mappings is not None and - len(provider.attribute_mappings) > 0): - attribute_mappings = provider.attribute_mappings - else: - attribute_mappings = self.cfg.default_attribute_mapping - if (provider.allowed_attributes is not None and - len(provider.allowed_attributes) > 0): - allowed_attributes = provider.allowed_attributes - else: - allowed_attributes = self.cfg.default_allowed_attributes - self.debug("Allowed attrs: %s" % allowed_attributes) - self.debug("Mapping: %s" % attribute_mappings) - policy = Policy(attribute_mappings, allowed_attributes) - userattrs = us.get_user_attrs() - mappedattrs, _ = policy.map_attributes(userattrs) - attributes = policy.filter_attributes(mappedattrs) - - if '_groups' in attributes and 'groups' not in attributes: - attributes['groups'] = attributes['_groups'] - - self.debug("%s's attributes: %s" % (user.name, attributes)) - # The saml-core-2.0-os specification section 2.7.3 requires # the AttributeStatement element to be non-empty. if attributes: diff --git a/tests/testmapping.py b/tests/testmapping.py index 6daa636..4b2acbd 100755 --- a/tests/testmapping.py +++ b/tests/testmapping.py @@ -83,14 +83,15 @@ def check_info_plugin(s, idp_name, urlbase, expected): # are no unexpected MELLON_ vars, and drop the _0 version. data = convert_to_dict(page.text) - data.pop('MELLON_IDP') - data.pop('MELLON_NAME_ID') - for key in expected: item = data.pop('MELLON_' + key) if item != expected[key]: raise ValueError('Expected %s, got %s' % (expected[key], item)) + # Ignore these attributes if they weren't tested for + data.pop('MELLON_IDP', None) + data.pop('MELLON_NAME_ID', None) + if len(data) > 0: raise ValueError('Unexpected values %s' % data) @@ -193,6 +194,7 @@ if __name__ == '__main__': try: print "testmapping: Test default mapping and attrs ...", expect = { + 'NAME_ID': user, 'fullname': 'Test User %s' % user, 'surname': user, 'givenname': 'Test User', @@ -268,8 +270,8 @@ if __name__ == '__main__': else: print " SUCCESS" + print "testmapping: Test SP allowed atributes ...", try: - print "testmapping: Test SP allowed atributes ...", expect = { 'fullname': 'Test User %s' % user, 'surname': user, @@ -298,14 +300,47 @@ if __name__ == '__main__': else: print " SUCCESS" + print "testmapping: Test SP attribute mapping ...", + try: + expect = { + 'wholename': 'Test User %s' % user, + 'fullname': 'Test User %s' % user, + 'surname': user, + 'givenname': 'Test User', + 'email': '%s@example.com' % user, + } + check_info_plugin(sess, idpname, spurl, expect) + except Exception, e: # pylint: disable=broad-except + print >> sys.stderr, " ERROR: %s" % repr(e) + sys.exit(1) + else: + print " SUCCESS" + + print "testmapping: Set SP username mapping ...", + try: + sess.set_attributes_and_mapping(idpname, + [['*', '*'], + ['fullname', 'wholename'], + ['email', '_username']], + ['wholename', 'givenname', + 'surname', + 'email', 'fullname'], + sp['name']) + except Exception, e: # pylint: disable=broad-except + print >> sys.stderr, " ERROR: %s" % repr(e) + sys.exit(1) + else: + print " SUCCESS" + + print "testmapping: Test SP username mapping ...", try: - print "testmapping: Test SP attribute mapping ...", expect = { 'wholename': 'Test User %s' % user, 'fullname': 'Test User %s' % user, 'surname': user, 'givenname': 'Test User', 'email': '%s@example.com' % user, + 'NAME_ID': '%s@example.com' % user, } check_info_plugin(sess, idpname, spurl, expect) except Exception, e: # pylint: disable=broad-except @@ -325,8 +360,8 @@ if __name__ == '__main__': else: print " SUCCESS" + print "testmapping: Test SP attr mapping with default allowed...", try: - print "testmapping: Test SP attr mapping with default allowed...", expect = { 'fullname': 'Test User %s' % user, 'surname': user, @@ -349,8 +384,8 @@ if __name__ == '__main__': else: print " SUCCESS" + print "testmapping: Test mapping, should be back to global...", try: - print "testmapping: Test mapping, should be back to global...", expect = { 'namefull': 'Test User %s' % user, 'surname': user,