Support for LDAP groups (bug #1092187)
Also covers Domain CRUD. Fixes Bug #1092187 Change-Id: If2266ed382edfedfad3eef450ce58640ca4b4657
This commit is contained in:
parent
a066b69fbe
commit
5cb8e1f2e5
@ -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')
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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'),
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user