Merge "Allow additional attribute mappings in ldap"

This commit is contained in:
Jenkins 2013-04-26 02:40:34 +00:00 committed by Gerrit Code Review
commit a78bc2e427
4 changed files with 77 additions and 2 deletions

View File

@ -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

View File

@ -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)

View File

@ -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 ' +
'<ldap_attribute>:<keystone_attribute>') % 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:

View File

@ -362,6 +362,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