Support for LDAP groups (bug #1092187)

Also covers Domain CRUD.

Fixes Bug #1092187

Change-Id: If2266ed382edfedfad3eef450ce58640ca4b4657
This commit is contained in:
Sahdev Zala 2013-02-21 16:11:12 -06:00
parent a066b69fbe
commit 5cb8e1f2e5
8 changed files with 284 additions and 52 deletions

View File

@ -116,7 +116,7 @@ class Group(Model):
"""
required_keys = ('id', 'name', 'domain_id')
optional_keys = ('description')
optional_keys = ('description',)
class Project(Model):
@ -162,3 +162,21 @@ class Trust(Model):
required_keys = ('id', 'trustor_user_id', 'trustee_user_id', 'project_id')
optional_keys = tuple('expires_at')
class Domain(Model):
"""Domain object.
Required keys:
id
name
Optional keys:
description
enabled (bool, default True)
"""
required_keys = ('id', 'name')
optional_keys = ('description', 'enabled')

View File

@ -310,12 +310,26 @@ register_str('group_id_attribute', group='ldap', default='cn')
register_str('group_name_attribute', group='ldap', default='ou')
register_str('group_member_attribute', group='ldap', default='member')
register_str('group_desc_attribute', group='ldap', default='description')
register_str('group_domain_id_attribute', group='ldap', default='domain_id')
register_str('group_domain_id_attribute', group='ldap',
default='businessCategory')
register_list('group_attribute_ignore', group='ldap', default='')
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_str('domain_tree_dn', group='ldap', default=None)
register_str('domain_filter', group='ldap', default=None)
register_str('domain_objectclass', group='ldap', default='groupOfNames')
register_str('domain_id_attribute', group='ldap', default='cn')
register_str('domain_name_attribute', group='ldap', default='ou')
register_str('domain_member_attribute', group='ldap', default='member')
register_str('domain_desc_attribute', group='ldap', default='description')
register_str('domain_enabled_attribute', group='ldap', default='enabled')
register_list('domain_attribute_ignore', group='ldap', default='')
register_bool('domain_allow_create', group='ldap', default=True)
register_bool('domain_allow_update', group='ldap', default=True)
register_bool('domain_allow_delete', group='ldap', default=True)
# pam
register_str('url', group='pam', default=None)
register_str('userid', group='pam', default=None)

View File

@ -28,7 +28,6 @@ from keystone import config
from keystone import exception
from keystone import identity
CONF = config.CONF
@ -44,6 +43,7 @@ class Identity(identity.Driver):
self.project = ProjectApi(CONF)
self.role = RoleApi(CONF)
self.group = GroupApi(CONF)
self.domain = DomainApi(CONF)
def get_connection(self, user=None, password=None):
if self.LDAP_URL.startswith('fake://'):
@ -238,6 +238,62 @@ class Identity(identity.Driver):
def delete_group(self, group_id):
return self.group.delete(group_id)
def add_user_to_group(self, user_id, group_id):
self.get_user(user_id)
self.get_group(group_id)
self.group.add_user(user_id, group_id)
def remove_user_from_group(self, user_id, group_id):
self.get_user(user_id)
self.get_group(group_id)
self.group.remove_user(user_id, group_id)
def list_groups_for_user(self, user_id):
self.get_user(user_id)
return self.group.list_user_groups(user_id)
def list_groups(self):
return self.group.get_all()
def list_users_in_group(self, group_id):
self.get_group(group_id)
return self.group.list_group_users(group_id)
def check_user_in_group(self, user_id, group_id):
self.get_user(user_id)
self.get_group(group_id)
user_refs = self.list_users_in_group(group_id)
found = False
for x in user_refs:
if x['id'] == user_id:
found = True
break
return found
def create_domain(self, domain_id, domain):
domain['name'] = clean.domain_name(domain['name'])
return self.domain.create(domain)
def get_domain(self, domain_id):
try:
return self.domain.get(domain_id)
except exception.NotFound:
raise exception.DomainNotFound(domain_id=domain_id)
def update_domain(self, domain_id, domain):
if 'name' in domain:
domain['name'] = clean.domain_name(domain['name'])
return self.domain.update(domain_id, domain)
def delete_domain(self, domain_id):
try:
return self.domain.delete(domain_id)
except ldap.NO_SUCH_OBJECT:
raise exception.DomainNotFound(domain_id=domain_id)
def list_domains(self):
return self.domain.get_all()
# TODO(termie): remove this and move cross-api calls into driver
class ApiShim(object):
@ -251,6 +307,7 @@ class ApiShim(object):
_project = None
_user = None
_group = None
_domain = None
def __init__(self, conf):
self.conf = conf
@ -275,9 +332,15 @@ class ApiShim(object):
@property
def group(self):
if not self.group:
self.group = GroupApi(self.conf)
return self.group
if not self._group:
self._group = GroupApi(self.conf)
return self._group
@property
def domain(self):
if not self._domain:
self._domain = DomainApi(self.conf)
return self._domain
# TODO(termie): remove this and move cross-api calls into driver
@ -300,6 +363,10 @@ class ApiShimMixin(object):
def group_api(self):
return self.api.group
@property
def domain_api(self):
return self.api.domain
# TODO(termie): turn this into a data object and move logic to driver
class UserApi(common_ldap.EnabledEmuMixIn, common_ldap.BaseLdap, ApiShimMixin):
@ -569,7 +636,6 @@ class RoleApi(common_ldap.BaseLdap, ApiShimMixin):
def create(self, values):
#values['id'] = values['name']
#delattr(values, 'name')
return super(RoleApi, self).create(values)
def add_user(self, role_id, user_id, tenant_id=None):
@ -736,9 +802,29 @@ class RoleApi(common_ldap.BaseLdap, ApiShimMixin):
pass
super(RoleApi, self).delete(id)
# TODO (spzala) - this is only placeholder for group and domain role support
# which will be added under bug 1101287
def roles_delete_subtree_by_type(self, id, type):
conn = self.get_connection()
query = '(objectClass=%s)' % self.object_class
dn = None
if type == 'Group':
dn = self.group_api._id_to_dn(id)
if type == 'Domain':
dn = self.domain_api._id_to_dn(id)
if dn:
try:
roles = conn.search_s(dn, ldap.SCOPE_ONELEVEL,
query, ['%s' % '1.1'])
for role_dn, _ in roles:
try:
conn.delete_s(role_dn)
except:
raise Exception
except ldap.NO_SUCH_OBJECT:
pass
# TODO (henry-nash) This is a placeholder for the full LDPA implementation
# This needs to be completed (see Bug #1092187)
class GroupApi(common_ldap.BaseLdap, ApiShimMixin):
DEFAULT_OU = 'ou=UserGroups'
DEFAULT_STRUCTURAL_CLASSES = []
@ -771,13 +857,15 @@ class GroupApi(common_ldap.BaseLdap, ApiShimMixin):
data = values.copy()
if data.get('id') is None:
data['id'] = uuid.uuid4().hex
if 'description' in data and data['description'] in ['', None]:
data.pop('description')
return super(GroupApi, self).create(data)
def delete(self, id):
if self.subtree_delete_enabled:
super(GroupApi, self).deleteTree(id)
else:
self.role_api.roles_delete_subtree_by_group(id)
self.role_api.roles_delete_subtree_by_type(id, 'Group')
super(GroupApi, self).delete(id)
def update(self, id, values):
@ -786,3 +874,112 @@ class GroupApi(common_ldap.BaseLdap, ApiShimMixin):
msg = _('Changing Name not supported by LDAP')
raise exception.NotImplemented(message=msg)
super(GroupApi, self).update(id, values, old_obj)
def add_user(self, user_id, group_id):
conn = self.get_connection()
try:
conn.modify_s(
self._id_to_dn(group_id),
[(ldap.MOD_ADD,
self.member_attribute,
self.user_api._id_to_dn(user_id))])
except ldap.TYPE_OR_VALUE_EXISTS:
msg = _('User %s is already a member of group %s'
% (user_id, group_id))
raise exception.Conflict(msg)
def remove_user(self, user_id, group_id):
conn = self.get_connection()
try:
conn.modify_s(
self._id_to_dn(group_id),
[(ldap.MOD_DELETE,
self.member_attribute,
self.user_api._id_to_dn(user_id))])
except ldap.NO_SUCH_ATTRIBUTE:
raise exception.UserNotFound(user_id=user_id)
def list_user_groups(self, user_id):
"""Returns a list of groups a user has access to"""
user_dn = self.user_api._id_to_dn(user_id)
query = '(%s=%s)' % (self.member_attribute, user_dn)
memberships = self.get_all(query)
return memberships
def list_group_users(self, group_id):
"""Returns a list of users that belong to a group"""
query = '(objectClass=%s)' % self.object_class
conn = self.get_connection()
group_dn = self._id_to_dn(group_id)
try:
attrs = conn.search_s(group_dn,
ldap.SCOPE_BASE,
query, ['%s' % self.member_attribute])
except ldap.NO_SUCH_OBJECT:
return []
users = []
for dn, member in attrs:
user_dns = member[self.member_attribute]
for user_dn in user_dns:
if self.use_dumb_member and user_dn == self.dumb_member:
continue
user_id = self.user_api._dn_to_id(user_dn)
users.append(self.user_api.get(user_id))
return users
class DomainApi(common_ldap.BaseLdap, ApiShimMixin):
DEFAULT_OU = 'ou=Domains'
DEFAULT_STRUCTURAL_CLASSES = []
DEFAULT_OBJECTCLASS = 'groupOfNames'
DEFAULT_ID_ATTR = 'cn'
DEFAULT_MEMBER_ATTRIBUTE = 'member'
DEFAULT_ATTRIBUTE_IGNORE = []
options_name = 'domain'
attribute_mapping = {'name': 'ou',
'description': 'description',
'domainId': 'cn',
'enabled': 'enabled'}
model = models.Domain
def __init__(self, conf):
super(DomainApi, self).__init__(conf)
self.api = ApiShim(conf)
self.attribute_mapping['name'] = conf.ldap.domain_name_attribute
self.attribute_mapping['description'] = conf.ldap.domain_desc_attribute
self.attribute_mapping['enabled'] = conf.ldap.tenant_enabled_attribute
self.member_attribute = (getattr(conf.ldap, 'domain_member_attribute')
or self.DEFAULT_MEMBER_ATTRIBUTE)
self.attribute_ignore = (getattr(conf.ldap, 'domain_attribute_ignore')
or self.DEFAULT_ATTRIBUTE_IGNORE)
def get(self, id, filter=None):
"""Replaces exception.NotFound with exception.DomainNotFound."""
try:
return super(DomainApi, self).get(id, filter)
except exception.NotFound:
raise exception.DomainNotFound(domain_id=id)
def create(self, values):
self.affirm_unique(values)
data = values.copy()
if data.get('id') is None:
data['id'] = uuid.uuid4().hex
return super(DomainApi, self).create(data)
def delete(self, id):
if self.subtree_delete_enabled:
super(DomainApi, self).deleteTree(id)
else:
self.role_api.roles_delete_subtree_by_type(id, 'Domain')
super(DomainApi, self).delete(id)
def update(self, id, values):
try:
old_obj = self.get(id)
except exception.NotFound:
raise exception.DomainNotFound(domain_id=id)
if old_obj['name'] != values['name']:
msg = _('Changing Name not supported by LDAP')
raise exception.NotImplemented(message=msg)
super(DomainApi, self).update(id, values, old_obj)

View File

@ -41,6 +41,7 @@ def filter_user(user_ref):
user_ref.pop('password', None)
user_ref.pop('tenants', None)
user_ref.pop('groups', None)
user_ref.pop('domains', None)
try:
user_ref['extra'].pop('password', None)
user_ref['extra'].pop('tenants', None)

View File

@ -67,11 +67,12 @@ class LiveLDAPIdentity(test_backend_ldap.LDAPIdentity):
create_object(CONF.ldap.tenant_tree_dn,
{'objectclass': 'organizationalUnit',
'ou': 'Projects'})
# NOTE(crazed): This feature is currently being added
create_object("ou=Groups,%s" % CONF.ldap.suffix,
create_object(CONF.ldap.domain_tree_dn,
{'objectclass': 'organizationalUnit',
'ou': 'Groups'})
'ou': 'Domain'})
create_object(CONF.ldap.group_tree_dn,
{'objectclass': 'organizationalUnit',
'ou': 'UserGroups'})
def _set_config(self):
self.config([test.etcdir('keystone.conf.sample'),

View File

@ -2,7 +2,7 @@
url = fake://memory
user = cn=Admin
password = password
backend_entities = ['Tenant', 'User', 'UserRoleAssociation', 'Role']
backend_entities = ['Tenant', 'User', 'UserRoleAssociation', 'Role', 'Group', 'Domain']
suffix = cn=example,cn=com
[identity]

View File

@ -3,8 +3,10 @@ url = ldap://localhost
user = dc=Manager,dc=openstack,dc=org
password = test
suffix = dc=openstack,dc=org
group_tree_dn = ou=UserGroups,dc=openstack,dc=org
role_tree_dn = ou=Roles,dc=openstack,dc=org
tenant_tree_dn = ou=Projects,dc=openstack,dc=org
domain_tree_dn = ou=Domains,dc=openstack,dc=org
user_tree_dn = ou=Users,dc=openstack,dc=org
tenant_enabled_emulation = True
user_enabled_emulation = True

View File

@ -359,29 +359,43 @@ class LDAPIdentity(test.TestCase, test_backend.IdentityTests):
# TODO (henry-nash) These need to be removed when the full LDAP implementation
# is submitted - see Bugs 1092187, 1101287, 1101276, 1101289
# (spzala)The group and domain crud tests below override the standard ones
# in test_backend.py so that we can exclude the update name test, since we
# do not yet support the update of either group or domain names with LDAP.
# In the tests below, the update is demonstrated by updating description.
# Refer to bug 1136403 for more detail.
def test_group_crud(self):
raise nose.exc.SkipTest('Blocked by bug 1092187')
group = {'id': uuid.uuid4().hex, 'domain_id': uuid.uuid4().hex,
'name': uuid.uuid4().hex, 'description': uuid.uuid4().hex}
self.identity_api.create_group(group['id'], group)
group_ref = self.identity_api.get_group(group['id'])
self.assertDictEqual(group_ref, group)
group['description'] = uuid.uuid4().hex
self.identity_api.update_group(group['id'], group)
group_ref = self.identity_api.get_group(group['id'])
self.assertDictEqual(group_ref, group)
def test_add_user_to_group(self):
raise nose.exc.SkipTest('Blocked by bug 1092187')
self.identity_api.delete_group(group['id'])
self.assertRaises(exception.GroupNotFound,
self.identity_api.get_group,
group['id'])
def test_add_user_to_group_404(self):
raise nose.exc.SkipTest('Blocked by bug 1092187')
def test_domain_crud(self):
domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex,
'enabled': True, 'description': uuid.uuid4().hex}
self.identity_api.create_domain(domain['id'], domain)
domain_ref = self.identity_api.get_domain(domain['id'])
self.assertDictEqual(domain_ref, domain)
domain['description'] = uuid.uuid4().hex
self.identity_api.update_domain(domain['id'], domain)
domain_ref = self.identity_api.get_domain(domain['id'])
self.assertDictEqual(domain_ref, domain)
def test_check_user_in_group(self):
raise nose.exc.SkipTest('Blocked by bug 1092187')
def test_check_user_not_in_group(self):
raise nose.exc.SkipTest('Blocked by bug 1092187')
def test_list_users_in_group(self):
raise nose.exc.SkipTest('Blocked by bug 1092187')
def test_remove_user_from_group(self):
raise nose.exc.SkipTest('Blocked by bug 1092187')
def test_remove_user_from_group_404(self):
raise nose.exc.SkipTest('Blocked by bug 1092187')
self.identity_api.delete_domain(domain['id'])
self.assertRaises(exception.DomainNotFound,
self.identity_api.get_domain,
domain['id'])
def test_get_role_grant_by_user_and_project(self):
raise nose.exc.SkipTest('Blocked by bug 1101287')
@ -407,15 +421,6 @@ class LDAPIdentity(test.TestCase, test_backend.IdentityTests):
def test_get_and_remove_correct_role_grant_from_a_mix(self):
raise nose.exc.SkipTest('Blocked by bug 1101287')
def test_get_roles_for_user_and_domain(self):
raise nose.exc.SkipTest('Blocked by bug 1101276')
def test_get_roles_for_user_and_domain_404(self):
raise nose.exc.SkipTest('Blocked by bug 1101276')
def test_domain_crud(self):
raise nose.exc.SkipTest('Blocked by bug 1101276')
def test_project_crud(self):
# NOTE(topol): LDAP implementation does not currently support the
# updating of a project name so this method override
@ -467,12 +472,6 @@ class LDAPIdentity(test.TestCase, test_backend.IdentityTests):
def test_delete_group_with_user_project_domain_links(self):
raise nose.exc.SkipTest('Blocked by bug 1101287')
def test_list_groups(self):
raise nose.exc.SkipTest('Blocked by bug 1092187')
def test_list_domains(self):
raise nose.exc.SkipTest('Blocked by bug 1101276')
def test_list_user_projects(self):
raise nose.exc.SkipTest('Blocked by bug 1101287')
@ -485,9 +484,6 @@ class LDAPIdentity(test.TestCase, test_backend.IdentityTests):
def test_create_duplicate_project_name_in_different_domains(self):
raise nose.exc.SkipTest('Blocked by bug 1101276')
def test_create_duplicate_group_name_fails(self):
raise nose.exc.SkipTest('Blocked by bug 1092187')
def test_create_duplicate_group_name_in_different_domains(self):
raise nose.exc.SkipTest('Blocked by bug 1101276')
@ -509,6 +505,9 @@ class LDAPIdentity(test.TestCase, test_backend.IdentityTests):
def test_move_project_between_domains_with_clashing_names_fails(self):
raise nose.exc.SkipTest('Blocked by bug 1101276')
def test_get_roles_for_user_and_domain(self):
raise nose.exc.SkipTest('Blocked by bug 1101287')
class LDAPIdentityEnabledEmulation(LDAPIdentity):
def setUp(self):