diff options
Diffstat (limited to 'keystone/assignment/backends/ldap.py')
-rw-r--r-- | keystone/assignment/backends/ldap.py | 543 |
1 files changed, 543 insertions, 0 deletions
diff --git a/keystone/assignment/backends/ldap.py b/keystone/assignment/backends/ldap.py new file mode 100644 index 00000000..9b273e40 --- /dev/null +++ b/keystone/assignment/backends/ldap.py @@ -0,0 +1,543 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012-2013 OpenStack LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from __future__ import absolute_import + +import uuid + +import ldap as ldap + +from keystone import assignment +from keystone import clean +from keystone.common import dependency +from keystone.common import ldap as common_ldap +from keystone.common import logging +from keystone.common import models +from keystone import config +from keystone import exception +from keystone.identity.backends import ldap as ldap_identity + + +CONF = config.CONF +LOG = logging.getLogger(__name__) + +DEFAULT_DOMAIN = { + 'id': CONF.identity.default_domain_id, + 'name': 'Default', + 'enabled': True +} + + +@dependency.requires('identity_api') +class Assignment(assignment.Driver): + def __init__(self): + super(Assignment, self).__init__() + self.LDAP_URL = CONF.ldap.url + self.LDAP_USER = CONF.ldap.user + self.LDAP_PASSWORD = CONF.ldap.password + self.suffix = CONF.ldap.suffix + + #These are the only deep dependency from assignment back + #to identity. The assumption is that if you are using + #LDAP for assignments, you are using it for Id as well. + self.user = ldap_identity.UserApi(CONF) + self.group = ldap_identity.GroupApi(CONF) + + self.project = ProjectApi(CONF) + self.role = RoleApi(CONF) + self._identity_api = None + + def get_project(self, tenant_id): + return self._set_default_domain(self.project.get(tenant_id)) + + def list_projects(self, domain_id=None): + # We don't support multiple domains within this driver, so ignore + # any domain passed. + return self._set_default_domain(self.project.get_all()) + + def get_project_by_name(self, tenant_name, domain_id): + self._validate_default_domain_id(domain_id) + return self._set_default_domain(self.project.get_by_name(tenant_name)) + + def create_project(self, tenant_id, tenant): + tenant = self._validate_default_domain(tenant) + tenant['name'] = clean.project_name(tenant['name']) + data = tenant.copy() + if 'id' not in data or data['id'] is None: + data['id'] = str(uuid.uuid4().hex) + if 'description' in data and data['description'] in ['', None]: + data.pop('description') + return self._set_default_domain(self.project.create(data)) + + def update_project(self, tenant_id, tenant): + tenant = self._validate_default_domain(tenant) + if 'name' in tenant: + tenant['name'] = clean.project_name(tenant['name']) + return self._set_default_domain(self.project.update(tenant_id, tenant)) + + def _get_metadata(self, user_id=None, tenant_id=None, + domain_id=None, group_id=None): + + def _get_roles_for_just_user_and_project(user_id, tenant_id): + self.identity_api.get_user(user_id) + self.get_project(tenant_id) + user_dn = self.user._id_to_dn(user_id) + return [self.role._dn_to_id(a.role_dn) + for a in self.role.get_role_assignments + (self.project._id_to_dn(tenant_id)) + if a.user_dn == user_dn] + + if domain_id is not None: + msg = 'Domain metadata not supported by LDAP' + raise exception.NotImplemented(message=msg) + if (not self.get_project(tenant_id) or + not self.identity_api.get_user(user_id)): + return {} + + metadata_ref = _get_roles_for_just_user_and_project(user_id, tenant_id) + if not metadata_ref: + return {} + return {'roles': [self._role_to_dict(r, False) for r in metadata_ref]} + + def get_role(self, role_id): + return self.role.get(role_id) + + def list_roles(self): + return self.role.get_all() + + def get_projects_for_user(self, user_id): + self.identity_api.get_user(user_id) + user_dn = self.user._id_to_dn(user_id) + associations = (self.role.list_project_roles_for_user + (user_dn, self.project.tree_dn)) + return [p['id'] for p in + self.project.get_user_projects(user_dn, associations)] + + def get_project_users(self, tenant_id): + self.get_project(tenant_id) + tenant_dn = self.project._id_to_dn(tenant_id) + rolegrants = self.role.get_role_assignments(tenant_dn) + users = [self.user.get_filtered(self.user._dn_to_id(user_id)) + for user_id in + self.project.get_user_dns(tenant_id, rolegrants)] + return self._set_default_domain(users) + + def _subrole_id_to_dn(self, role_id, tenant_id): + if tenant_id is None: + return self.role._id_to_dn(role_id) + else: + return '%s=%s,%s' % (self.role.id_attr, + ldap.dn.escape_dn_chars(role_id), + self.project._id_to_dn(tenant_id)) + + def add_role_to_user_and_project(self, user_id, tenant_id, role_id): + self.identity_api.get_user(user_id) + self.get_project(tenant_id) + self.get_role(role_id) + user_dn = self.user._id_to_dn(user_id) + role_dn = self._subrole_id_to_dn(role_id, tenant_id) + self.role.add_user(role_id, role_dn, user_dn, user_id, tenant_id) + tenant_dn = self.project._id_to_dn(tenant_id) + return UserRoleAssociation( + role_dn=role_dn, + user_dn=user_dn, + tenant_dn=tenant_dn) + + def _create_metadata(self, user_id, tenant_id, metadata): + return {} + + def create_role(self, role_id, role): + try: + self.get_role(role_id) + except exception.NotFound: + pass + else: + msg = 'Duplicate ID, %s.' % role_id + raise exception.Conflict(type='role', details=msg) + + try: + self.role.get_by_name(role['name']) + except exception.NotFound: + pass + else: + msg = 'Duplicate name, %s.' % role['name'] + raise exception.Conflict(type='role', details=msg) + + return self.role.create(role) + + def delete_role(self, role_id): + return self.role.delete(role_id, self.project.tree_dn) + + def delete_project(self, tenant_id): + if self.project.subtree_delete_enabled: + self.project.deleteTree(id) + else: + tenant_dn = self.project._id_to_dn(tenant_id) + self.role.roles_delete_subtree_by_project(tenant_dn) + self.project.delete(tenant_id) + + def remove_role_from_user_and_project(self, user_id, tenant_id, role_id): + role_dn = self._subrole_id_to_dn(role_id, tenant_id) + return self.role.delete_user(role_dn, + self.user._id_to_dn(user_id), + self.project._id_to_dn(tenant_id), + user_id, role_id) + + def update_role(self, role_id, role): + self.get_role(role_id) + self.role.update(role_id, role) + + def create_domain(self, domain_id, domain): + if domain_id == CONF.identity.default_domain_id: + msg = 'Duplicate ID, %s.' % domain_id + raise exception.Conflict(type='domain', details=msg) + raise exception.Forbidden('Domains are read-only against LDAP') + + def get_domain(self, domain_id): + self._validate_default_domain_id(domain_id) + return DEFAULT_DOMAIN + + def update_domain(self, domain_id, domain): + self._validate_default_domain_id(domain_id) + raise exception.Forbidden('Domains are read-only against LDAP') + + def delete_domain(self, domain_id): + self._validate_default_domain_id(domain_id) + raise exception.Forbidden('Domains are read-only against LDAP') + + def list_domains(self): + return [assignment.DEFAULT_DOMAIN] + +#Bulk actions on User From identity + def delete_user(self, user_id): + user_dn = self.user._id_to_dn(user_id) + for ref in self.role.list_global_roles_for_user(user_dn): + self.role.delete_user(ref.role_dn, ref.user_dn, ref.project_dn, + user_id, self.role._dn_to_id(ref.role_dn)) + for ref in self.role.list_project_roles_for_user(user_dn, + self.project.tree_dn): + self.role.delete_user(ref.role_dn, ref.user_dn, ref.project_dn, + user_id, self.role._dn_to_id(ref.role_dn)) + + user = self.user.get(user_id) + if hasattr(user, 'tenant_id'): + self.project.remove_user(user.tenant_id, + self.user._id_to_dn(user_id)) + + #LDAP assignments only supports LDAP identity. Assignments under identity + #are already deleted + def delete_group(self, group_id): + if not self.group.subtree_delete_enabled: + # TODO(spzala): this is only placeholder for group and domain + # role support which will be added under bug 1101287 + conn = self.group.get_connection() + query = '(objectClass=%s)' % self.group.object_class + dn = None + dn = self.group._id_to_dn(id) + if dn: + try: + roles = conn.search_s(dn, ldap.SCOPE_ONELEVEL, + query, ['%s' % '1.1']) + for role_dn, _ in roles: + conn.delete_s(role_dn) + except ldap.NO_SUCH_OBJECT: + pass + + +# TODO(termie): turn this into a data object and move logic to driver +class ProjectApi(common_ldap.EnabledEmuMixIn, common_ldap.BaseLdap): + DEFAULT_OU = 'ou=Groups' + DEFAULT_STRUCTURAL_CLASSES = [] + DEFAULT_OBJECTCLASS = 'groupOfNames' + DEFAULT_ID_ATTR = 'cn' + DEFAULT_MEMBER_ATTRIBUTE = 'member' + DEFAULT_ATTRIBUTE_IGNORE = [] + NotFound = exception.ProjectNotFound + notfound_arg = 'project_id' # NOTE(yorik-sar): while options_name = tenant + options_name = 'tenant' + attribute_mapping = {'name': 'ou', + 'description': 'description', + 'tenantId': 'cn', + 'enabled': 'enabled', + 'domain_id': 'domain_id'} + model = models.Project + + def __init__(self, conf): + super(ProjectApi, self).__init__(conf) + self.attribute_mapping['name'] = conf.ldap.tenant_name_attribute + self.attribute_mapping['description'] = conf.ldap.tenant_desc_attribute + self.attribute_mapping['enabled'] = conf.ldap.tenant_enabled_attribute + self.attribute_mapping['domain_id'] = ( + conf.ldap.tenant_domain_id_attribute) + self.member_attribute = (getattr(conf.ldap, 'tenant_member_attribute') + or self.DEFAULT_MEMBER_ATTRIBUTE) + self.attribute_ignore = (getattr(conf.ldap, 'tenant_attribute_ignore') + or self.DEFAULT_ATTRIBUTE_IGNORE) + + def create(self, values): + self.affirm_unique(values) + data = values.copy() + if data.get('id') is None: + data['id'] = uuid.uuid4().hex + return super(ProjectApi, self).create(data) + + def get_user_projects(self, user_dn, associations): + """Returns list of tenants a user has access to + """ + + project_ids = set() + for assoc in associations: + project_ids.add(self._dn_to_id(assoc.project_dn)) + projects = [] + for project_id in project_ids: + #slower to get them one at a time, but a huge list could blow out + #the connection. This is the safer way + projects.append(self.get(project_id)) + return projects + + def add_user(self, tenant_id, user_dn): + conn = self.get_connection() + try: + conn.modify_s( + self._id_to_dn(tenant_id), + [(ldap.MOD_ADD, + self.member_attribute, + user_dn)]) + except ldap.TYPE_OR_VALUE_EXISTS: + # As adding a user to a tenant is done implicitly in several + # places, and is not part of the exposed API, it's easier for us to + # just ignore this instead of raising exception.Conflict. + pass + + def remove_user(self, tenant_id, user_dn, user_id): + conn = self.get_connection() + try: + conn.modify_s(self._id_to_dn(tenant_id), + [(ldap.MOD_DELETE, + self.member_attribute, + user_dn)]) + except ldap.NO_SUCH_ATTRIBUTE: + raise exception.NotFound(user_id) + + def get_user_dns(self, tenant_id, rolegrants, role_dn=None): + tenant = self._ldap_get(tenant_id) + res = set() + if not role_dn: + # Get users who have default tenant mapping + for user_dn in tenant[1].get(self.member_attribute, []): + if self.use_dumb_member and user_dn == self.dumb_member: + continue + res.add(user_dn) + + # Get users who are explicitly mapped via a tenant + for rolegrant in rolegrants: + if role_dn is None or rolegrant.role_dn == role_dn: + res.add(rolegrant.user_dn) + return list(res) + + def update(self, id, values): + old_obj = self.get(id) + if old_obj['name'] != values['name']: + msg = 'Changing Name not supported by LDAP' + raise exception.NotImplemented(message=msg) + return super(ProjectApi, self).update(id, values, old_obj) + + +class UserRoleAssociation(object): + """Role Grant model.""" + + def __init__(self, user_dn=None, role_dn=None, tenant_dn=None, + *args, **kw): + self.user_dn = user_dn + self.role_dn = role_dn + self.project_dn = tenant_dn + + +class GroupRoleAssociation(object): + """Role Grant model.""" + + def __init__(self, group_dn=None, role_dn=None, tenant_dn=None, + *args, **kw): + self.group_dn = group_dn + self.role_dn = role_dn + self.project_dn = tenant_dn + + +# TODO(termie): turn this into a data object and move logic to driver +class RoleApi(common_ldap.BaseLdap): + DEFAULT_OU = 'ou=Roles' + DEFAULT_STRUCTURAL_CLASSES = [] + DEFAULT_OBJECTCLASS = 'organizationalRole' + DEFAULT_MEMBER_ATTRIBUTE = 'roleOccupant' + DEFAULT_ATTRIBUTE_IGNORE = [] + NotFound = exception.RoleNotFound + options_name = 'role' + attribute_mapping = {'name': 'ou', + #'serviceId': 'service_id', + } + model = models.Role + + def __init__(self, conf): + super(RoleApi, self).__init__(conf) + self.attribute_mapping['name'] = conf.ldap.role_name_attribute + self.member_attribute = (getattr(conf.ldap, 'role_member_attribute') + or self.DEFAULT_MEMBER_ATTRIBUTE) + self.attribute_ignore = (getattr(conf.ldap, 'role_attribute_ignore') + or self.DEFAULT_ATTRIBUTE_IGNORE) + + def get(self, id, filter=None): + model = super(RoleApi, self).get(id, filter) + return model + + def create(self, values): + return super(RoleApi, self).create(values) + + def add_user(self, role_id, role_dn, user_dn, user_id, tenant_id=None): + conn = self.get_connection() + try: + conn.modify_s(role_dn, [(ldap.MOD_ADD, + self.member_attribute, user_dn)]) + except ldap.TYPE_OR_VALUE_EXISTS: + msg = ('User %s already has role %s in tenant %s' + % (user_id, role_id, tenant_id)) + raise exception.Conflict(type='role grant', details=msg) + except ldap.NO_SUCH_OBJECT: + if tenant_id is None or self.get(role_id) is None: + raise Exception(_("Role %s not found") % (role_id,)) + + attrs = [('objectClass', [self.object_class]), + (self.member_attribute, [user_dn])] + + if self.use_dumb_member: + attrs[1][1].append(self.dumb_member) + try: + conn.add_s(role_dn, attrs) + except Exception as inst: + raise inst + + def delete_user(self, role_dn, user_dn, tenant_dn, + user_id, role_id): + conn = self.get_connection() + try: + conn.modify_s(role_dn, [(ldap.MOD_DELETE, + self.member_attribute, user_dn)]) + except ldap.NO_SUCH_OBJECT: + if tenant_dn is None: + raise exception.RoleNotFound(role_id=role_id) + attrs = [('objectClass', [self.object_class]), + (self.member_attribute, [user_dn])] + + if self.use_dumb_member: + attrs[1][1].append(self.dumb_member) + try: + conn.add_s(role_dn, attrs) + except Exception as inst: + raise inst + except ldap.NO_SUCH_ATTRIBUTE: + raise exception.UserNotFound(user_id=user_id) + + def get_role_assignments(self, tenant_dn): + conn = self.get_connection() + query = '(objectClass=%s)' % self.object_class + + try: + roles = conn.search_s(tenant_dn, ldap.SCOPE_ONELEVEL, query) + except ldap.NO_SUCH_OBJECT: + return [] + + res = [] + for role_dn, attrs in roles: + try: + user_dns = attrs[self.member_attribute] + except KeyError: + continue + for user_dn in user_dns: + if self.use_dumb_member and user_dn == self.dumb_member: + continue + res.append(UserRoleAssociation( + user_dn=user_dn, + role_dn=role_dn, + tenant_dn=tenant_dn)) + + return res + + def list_global_roles_for_user(self, user_dn): + roles = self.get_all('(%s=%s)' % (self.member_attribute, user_dn)) + return [UserRoleAssociation( + role_dn=role.dn, + user_dn=user_dn) for role in roles] + + def list_project_roles_for_user(self, user_dn, project_subtree): + conn = self.get_connection() + query = '(&(objectClass=%s)(%s=%s))' % (self.object_class, + self.member_attribute, + user_dn) + try: + roles = conn.search_s(project_subtree, + ldap.SCOPE_SUBTREE, + query) + except ldap.NO_SUCH_OBJECT: + return [] + + res = [] + for role_dn, _ in roles: + #ldap.dn.dn2str returns an array, where the first + #element is the first segment. + #For a role assignment, this contains the role ID, + #The remainder is the DN of the tenant. + tenant = ldap.dn.str2dn(role_dn) + tenant.pop(0) + tenant_dn = ldap.dn.dn2str(tenant) + res.append(UserRoleAssociation( + user_dn=user_dn, + role_dn=role_dn, + tenant_dn=tenant_dn)) + return res + + def roles_delete_subtree_by_project(self, tenant_dn): + conn = self.get_connection() + query = '(objectClass=%s)' % self.object_class + try: + roles = conn.search_s(tenant_dn, ldap.SCOPE_ONELEVEL, query) + for role_dn, _ in roles: + try: + conn.delete_s(role_dn) + except Exception as inst: + raise inst + except ldap.NO_SUCH_OBJECT: + pass + + def update(self, role_id, role): + if role['id'] != role_id: + raise exception.ValidationError('Cannot change role ID') + try: + old_name = self.get_by_name(role['name']) + raise exception.Conflict('Cannot duplicate name %s' % old_name) + except exception.NotFound: + pass + return super(RoleApi, self).update(role_id, role) + + def delete(self, id, tenant_dn): + conn = self.get_connection() + query = '(&(objectClass=%s)(%s=%s))' % (self.object_class, + self.id_attr, id) + try: + for role_dn, _ in conn.search_s(tenant_dn, + ldap.SCOPE_SUBTREE, + query): + conn.delete_s(role_dn) + except ldap.NO_SUCH_OBJECT: + pass + super(RoleApi, self).delete(id) |