From f452c3d6b15123ca1b383f1d200f4cb406c81852 Mon Sep 17 00:00:00 2001 From: Allan Feid Date: Thu, 21 Mar 2013 14:19:48 -0400 Subject: [PATCH] Allow additional attribute mappings in ldap This is needed as a work around for objectclasses that require additional attributes other than just what is supplied in user_id_attribute and user_name_attribute. Change-Id: Ie6cdd0534b8389f62f98fdca7d19bc0feb9c131f Fixes: bug #1158077 --- etc/keystone.conf.sample | 12 ++++++++++++ keystone/common/config.py | 11 +++++++++++ keystone/common/ldap/core.py | 36 ++++++++++++++++++++++++++++++++++-- tests/test_backend_ldap.py | 20 ++++++++++++++++++++ 4 files changed, 77 insertions(+), 2 deletions(-) diff --git a/etc/keystone.conf.sample b/etc/keystone.conf.sample index 8cb2cca354..55b265aaf5 100644 --- a/etc/keystone.conf.sample +++ b/etc/keystone.conf.sample @@ -226,6 +226,18 @@ # tls_cacertdir = # tls_req_cert = demand +# Additional attribute mappings can be used to map ldap attributes to internal +# keystone attributes. This allows keystone to fulfill ldap objectclass +# requirements. An example to map the description and gecos attributes to a +# user's name would be: +# user_additional_attribute_mapping = description:name, gecos:name +# +# domain_additional_attribute_mapping = +# group_additional_attribute_mapping = +# role_additional_attribute_mapping = +# project_additional_attribute_mapping = +# user_additional_attribute_mapping = + [auth] methods = password,token password = keystone.auth.plugins.password.Password diff --git a/keystone/common/config.py b/keystone/common/config.py index d7b6ff7348..d3f6741ce9 100644 --- a/keystone/common/config.py +++ b/keystone/common/config.py @@ -314,6 +314,8 @@ def configure(): register_bool('user_allow_delete', group='ldap', default=True) register_bool('user_enabled_emulation', group='ldap', default=False) register_str('user_enabled_emulation_dn', group='ldap', default=None) + register_list( + 'user_additional_attribute_mapping', group='ldap', default=None) register_str('tenant_tree_dn', group='ldap', default=None) register_str('tenant_filter', group='ldap', default=None) @@ -331,6 +333,8 @@ def configure(): register_bool('tenant_allow_delete', group='ldap', default=True) register_bool('tenant_enabled_emulation', group='ldap', default=False) register_str('tenant_enabled_emulation_dn', group='ldap', default=None) + register_list( + 'tenant_additional_attribute_mapping', group='ldap', default=None) register_str('role_tree_dn', group='ldap', default=None) register_str('role_filter', group='ldap', default=None) @@ -343,6 +347,8 @@ def configure(): register_bool('role_allow_create', group='ldap', default=True) register_bool('role_allow_update', group='ldap', default=True) register_bool('role_allow_delete', group='ldap', default=True) + register_list( + 'role_additional_attribute_mapping', group='ldap', default=None) register_str('group_tree_dn', group='ldap', default=None) register_str('group_filter', group='ldap', default=None) @@ -357,6 +363,8 @@ def configure(): register_bool('group_allow_create', group='ldap', default=True) register_bool('group_allow_update', group='ldap', default=True) register_bool('group_allow_delete', group='ldap', default=True) + register_list( + 'group_additional_attribute_mapping', group='ldap', default=None) register_str('domain_tree_dn', group='ldap', default=None) register_str('domain_filter', group='ldap', default=None) @@ -372,6 +380,9 @@ def configure(): register_bool('domain_allow_delete', group='ldap', default=True) register_bool('domain_enabled_emulation', group='ldap', default=False) register_str('domain_enabled_emulation_dn', group='ldap', default=None) + register_list( + 'domain_additional_attribute_mapping', group='ldap', default=None) + register_str('tls_cacertfile', group='ldap', default=None) register_str('tls_cacertdir', group='ldap', default=None) register_bool('use_tls', group='ldap', default=False) diff --git a/keystone/common/ldap/core.py b/keystone/common/ldap/core.py index 80189b10b8..7113fbbb9b 100644 --- a/keystone/common/ldap/core.py +++ b/keystone/common/ldap/core.py @@ -104,6 +104,7 @@ class BaseLdap(object): DEFAULT_ID_ATTR = 'cn' DEFAULT_OBJECTCLASS = None DEFAULT_FILTER = None + DEFAULT_EXTRA_ATTR_MAPPING = [] DUMB_MEMBER_DN = 'cn=dumb,dc=nonexistent' NotFound = None notfound_arg = None @@ -140,6 +141,12 @@ class BaseLdap(object): self.object_class = (getattr(conf.ldap, objclass) or self.DEFAULT_OBJECTCLASS) + attr_mapping_opt = ('%s_additional_attribute_mapping' % + self.options_name) + attr_mapping = (getattr(conf.ldap, attr_mapping_opt) + or self.DEFAULT_EXTRA_ATTR_MAPPING) + self.extra_attr_mapping = self._parse_extra_attrs(attr_mapping) + filter = '%s_filter' % self.options_name self.filter = getattr(conf.ldap, filter) or self.DEFAULT_FILTER @@ -169,6 +176,25 @@ class BaseLdap(object): else: return self.NotFound(**{self.notfound_arg: object_id}) + def _parse_extra_attrs(self, option_list): + mapping = {} + for item in option_list: + try: + ldap_attr, attr_map = item.split(':') + except Exception: + LOG.warn(_('Invalid additional attribute mapping: "%s". ' + 'Format must be ' + + ':') % item) + continue + if attr_map not in self.attribute_mapping: + LOG.warn(_('Invalid additional attribute mapping: "%(item)s". ' + 'Value "%(attr_map)s" must use one of %(keys)s.') % + {'item': item, 'attr_map': attr_map, + 'keys': ', '.join(self.attribute_mapping.keys())}) + continue + mapping[ldap_attr] = attr_map + return mapping + def get_connection(self, user=None, password=None): if self.LDAP_URL.startswith('fake://'): conn = fakeldap.FakeLdap(self.LDAP_URL) @@ -272,6 +298,11 @@ class BaseLdap(object): if v is not None: attr_type = self.attribute_mapping.get(k, k) attrs.append((attr_type, [v])) + extra_attrs = [attr for attr, name + in self.extra_attr_mapping.iteritems() + if name == k] + for attr in extra_attrs: + attrs.append((attr, [v])) if 'groupOfNames' in object_classes and self.use_dumb_member: attrs.append(('member', [self.dumb_member])) @@ -289,8 +320,9 @@ class BaseLdap(object): 'filter': (filter or self.filter or ''), 'object_class': self.object_class}) try: - res = conn.search_s(self.tree_dn, self.LDAP_SCOPE, query, - self.attribute_mapping.values()) + attrs = list(set((self.attribute_mapping.values() + + self.extra_attr_mapping.keys()))) + res = conn.search_s(self.tree_dn, self.LDAP_SCOPE, query, attrs) except ldap.NO_SUCH_OBJECT: return None try: diff --git a/tests/test_backend_ldap.py b/tests/test_backend_ldap.py index 6db626d8f7..b0ce39204c 100644 --- a/tests/test_backend_ldap.py +++ b/tests/test_backend_ldap.py @@ -364,6 +364,26 @@ class LDAPIdentity(test.TestCase, test_backend.IdentityTests): 'Invalid LDAP deref option: %s\.' % CONF.ldap.alias_dereferencing, identity.backends.ldap.Identity) + def test_user_extra_attribute_mapping(self): + CONF.ldap.user_additional_attribute_mapping = ['description:name'] + self.identity_api = identity.backends.ldap.Identity() + user = { + 'id': 'extra_attributes', + 'name': 'EXTRA_ATTRIBUTES', + 'password': 'extra', + } + self.identity_api.create_user(user['id'], user) + dn, attrs = self.identity_api.user._ldap_get(user['id']) + self.assertTrue(user['name'] in attrs['description']) + + def test_parse_extra_attribute_mapping(self): + option_list = ['description:name', 'gecos:password', + 'fake:invalid', 'invalid1', 'invalid2:', + 'description:name:something'] + mapping = self.identity_api.user._parse_extra_attrs(option_list) + expected_dict = {'description': 'name', 'gecos': 'password'} + self.assertDictEqual(expected_dict, mapping) + # TODO (henry-nash) These need to be removed when the full LDAP implementation # is submitted - see Bugs 1092187, 1101287, 1101276, 1101289