diff --git a/.gitignore b/.gitignore index c4dff7bbb2..a08c9b54ef 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ .pydevproject/ .settings/ keystone.db +ldap.db keystone.token.db .*.swp *.log diff --git a/keystone/backends/ldap/__init__.py b/keystone/backends/ldap/__init__.py new file mode 100644 index 0000000000..42766600b1 --- /dev/null +++ b/keystone/backends/ldap/__init__.py @@ -0,0 +1,16 @@ +import ldap + +import keystone.backends.api as top_api +import keystone.backends.models as top_models +from keystone import utils + +from . import api +from . import models + + +def configure_backend(options): + api_obj = api.API(options) + for name in api_obj.apis: + top_api.set_value(name, getattr(api_obj, name)) + for model_name in models.__all__: + top_models.set_value(model_name, getattr(models, model_name)) diff --git a/keystone/backends/ldap/api/__init__.py b/keystone/backends/ldap/api/__init__.py new file mode 100644 index 0000000000..9244fdbb10 --- /dev/null +++ b/keystone/backends/ldap/api/__init__.py @@ -0,0 +1,25 @@ +import ldap + +from .. import fakeldap +from .tenant import TenantAPI +from .user import UserAPI +from .role import RoleAPI + +class API(object): + apis = ['tenant', 'user', 'role'] + + def __init__(self, options): + self.LDAP_URL = options['ldap_url'] + self.LDAP_USER = options['ldap_user'] + self.LDAP_PASSWORD = options['ldap_password'] + self.tenant = TenantAPI(self, options) + self.user = UserAPI(self, options) + self.role = RoleAPI(self, options) + + def get_connection(self): + if self.LDAP_URL.startswith('fake://'): + conn = fakeldap.initialize(self.LDAP_URL) + else: + conn = ldap.initialize(self.LDAP_URL) + conn.simple_bind_s(self.LDAP_USER, self.LDAP_PASSWORD) + return conn diff --git a/keystone/backends/ldap/api/base.py b/keystone/backends/ldap/api/base.py new file mode 100644 index 0000000000..0018b43670 --- /dev/null +++ b/keystone/backends/ldap/api/base.py @@ -0,0 +1,150 @@ +import ldap + + +def _get_redirect(cls, method): + def inner(self, *args): + return getattr(cls(), method)(*args) + return inner + + +def add_redirects(loc, cls, methods): + for method in methods: + loc[method] = _get_redirect(cls, method) + + +class BaseLdapAPI(object): + DEFAULT_TREE_DN = None + options_name = None + object_class = 'top' + model = None + attribute_mapping = {} + attribute_ignore = [] + + def __init__(self, api, options): + self.api = api + self.tree_dn = options.get(self.options_name, self.DEFAULT_TREE_DN) + + def _id_to_dn(self, id): + return 'cn=%s,%s' % (ldap.dn.escape_dn_chars(str(id)), self.tree_dn) + + def _ldap_res_to_model(self, res): + obj = self.model(id=ldap.dn.str2dn(res[0])[0][0][1]) + for k in obj: + if k in self.attribute_ignore: + continue + try: + v = res[1][self.attribute_mapping.get(k, k)] + except KeyError: + pass + else: + obj[k] = v[0] + return obj + + def create(self, values): + conn = self.api.get_connection() + attrs = [('objectClass', [self.object_class])] + for k, v in values.iteritems(): + if k == 'id' or k in self.attribute_ignore: + continue + if v is not None: + attr_type = self.attribute_mapping.get(k, k) + attrs.append((attr_type, [v])) + conn.add_s(self._id_to_dn(values['id']), attrs) + return self.model(values) + + def _ldap_get(self, id, filter=None): + conn = self.api.get_connection() + query = '(objectClass=%s)' % (self.object_class,) + if filter is not None: + query = '(&%s%s)' % (filter, query) + try: + res = conn.search_s(self._id_to_dn(id), ldap.SCOPE_BASE, query) + except ldap.NO_SUCH_OBJECT: + return None + try: + return res[0] + except IndexError: + return None + + def _ldap_get_all(self, filter=None): + conn = self.api.get_connection() + query = '(objectClass=%s)' % (self.object_class,) + if filter is not None: + query = '(&%s%s)' % (filter, query) + try: + return conn.search_s(self.tree_dn, ldap.SCOPE_ONELEVEL, query) + except ldap.NO_SUCH_OBJECT: + return [] + + def get(self, id, filter=None): + res = self._ldap_get(id, filter) + if res is None: + return None + else: + return self._ldap_res_to_model(res) + + def get_all(self, filter=None): + return map(self._ldap_res_to_model, self._ldap_get_all(filter)) + + def get_page(self, marker, limit): + return self._get_page(marker, limit, self.get_all()) + + def get_page_markers(self, marker, limit): + return self._get_page_markers(marker, limit, self.get_all()) + + def _get_page(self, marker, limit, lst, key=lambda e:e.id): + lst.sort(key=key) + if not marker: + return lst[:limit] + else: + return filter(lambda e: key(e) > marker, lst)[:limit] + + def _get_page_markers(self, marker, limit, lst, key=lambda e:e.id): + if len(lst) < limit: + return (None, None) + lst.sort(key=key) + if marker is None: + if len(lst) <= limit + 1: + nxt = None + else: + nxt = key(lst[limit]) + return (None, nxt) + for i, item in izip(count(), lst): + k = key(item) + if k >= marker: + exact = k == marker + break + if i <= limit: + prv = None + else: + prv = key(lst[i-limit]) + if i + limit >= len(lst) - 1: + nxt = None + else: + nxt = key(lst[i+limit]) + return (prv, nxt) + + def update(self, id, values, old_obj=None): + if old_obj is None: + old_obj = self.get(id) + modlist = [] + for k, v in values.iteritems(): + if k == 'id' or k in self.attribute_ignore: + continue + if v is None: + if old_obj[k] is not None: + modlist.append((ldap.MOD_DELETE, + self.attribute_mapping.get(k, k), None)) + else: + if old_obj[k] != v: + if old_obj[k] is None: + op = ldap.MOD_ADD + else: + op = ldap.MOD_REPLACE + modlist.append((op, self.attribute_mapping.get(k, k), [v])) + conn = self.api.get_connection() + conn.modify_s(self._id_to_dn(id), modlist) + + def delete(self, id): + conn = self.api.get_connection() + conn.delete_s(self._id_to_dn(id)) diff --git a/keystone/backends/ldap/api/role.py b/keystone/backends/ldap/api/role.py new file mode 100644 index 0000000000..b1bd7661ac --- /dev/null +++ b/keystone/backends/ldap/api/role.py @@ -0,0 +1,154 @@ +import ldap + +from keystone.backends.api import BaseTenantAPI +from keystone.common import exception + +from .. import models +from .base import BaseLdapAPI + +class RoleAPI(BaseLdapAPI, BaseTenantAPI): + DEFAULT_TREE_DN = 'ou=Groups,dc=example,dc=com' + options_name = 'role_tree_dn' + object_class = 'keystoneRole' + model = models.Role + attribute_mapping = { 'desc': 'description' } + + @staticmethod + def _create_ref(role_id, tenant_id, user_id): + role_id = '' if role_id is None else str(role_id) + tenant_id = '' if tenant_id is None else str(tenant_id) + user_id = '' if user_id is None else str(user_id) + return '%d-%d-%s%s%s' % (len(role_id), len(tenant_id), + role_id, tenant_id, user_id) + @staticmethod + def _explode_ref(role_ref): + a = role_ref.split('-', 2) + len_role = int(a[0]) + len_tenant = int(a[1]) + role_id = a[2][:len_role] + role_id = None if len(role_id) == 0 else str(role_id) + tenant_id = a[2][len_role:len_tenant+len_role] + tenant_id = None if len(tenant_id) == 0 else str(tenant_id) + user_id = a[2][len_tenant+len_role:] + user_id = None if len(user_id) == 0 else str(user_id) + return role_id, tenant_id, user_id + + def _subrole_id_to_dn(self, role_id, tenant_id): + if tenant_id is None: + return self._id_to_dn(role_id) + else: + return "cn=%s,%s" % (ldap.dn.escape_dn_chars(role_id), + self.api.tenant._id_to_dn(tenant_id)) + + def add_user(self, role_id, user_id, tenant_id=None): + user = self.api.user.get(user_id) + if user is None: + raise exception.NotFound("User %s not found" % (user_id,)) + role_dn = self._subrole_id_to_dn(role_id, tenant_id) + conn = self.api.get_connection() + user_dn = self.api.user._id_to_dn(user_id) + try: + conn.modify_s(role_dn, [(ldap.MOD_ADD, 'member', user_dn)]) + except ldap.TYPE_OR_VALUE_EXISTS: + raise exception.Duplicate( + "User %s already has role %s in tenant %s" % (user_id, + role_id, tenant_id)) + except ldap.NO_SUCH_OBJECT: + if tenant_id is None or self.get(role_id) is None: + raise exception.NotFound("Role %s not found" % (role_id,)) + attrs = [ + ('objectClass', 'keystoneTenantRole'), + ('member', user_dn), + ('role', self._id_to_dn(role_id)), + ] + conn.add_s(role_dn, attrs) + return models.UserRoleAssociation( + id=self._create_ref(role_id, tenant_id, user_id), + role_id=role_id, user_id=user_id, tenant_id=tenant_id) + + def get_role_assignments(self, tenant_id): + conn = self.api.get_connection() + query = '(objectClass=keystoneTenantRole)' + tenant_dn = self.api.tenant._id_to_dn(tenant_id) + 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['member'] + except KeyError: + continue + for user_dn in user_dns: + user_id=ldap.dn.str2dn(user_dn)[0][0][1] + role_id=ldap.dn.str2dn(role_dn)[0][0][1] + res.append(models.UserRoleAssociation( + id=self._create_ref(role_id, tenant_id, user_id), + user_id=user_id, + role_id=role_id, + tenant_id=tenant_id)) + return res + + def ref_get_all_global_roles(self, user_id): + user_dn = self.api.user._id_to_dn(user_id) + roles = self.get_all('(member=%s)' % (user_dn,)) + return [models.UserRoleAssociation( + id=self._create_ref(role.id, None, user_id), + role_id=role.id, + user_id=user_id) for role in roles] + + def ref_get_all_tenant_roles(self, user_id, tenant_id): + conn = self.api.get_connection() + user_dn = self.api.user._id_to_dn(user_id) + tenant_dn = self.api.tenant._id_to_dn(tenant_id) + query = '(&(objectClass=keystoneTenantRole)(member=%s))' % (user_dn,) + try: + roles = conn.search_s(tenant_dn, ldap.SCOPE_ONELEVEL, query) + except ldap.NO_SUCH_OBJECT: + return [] + res = [] + for role_dn, _ in roles: + role_id = ldap.dn.str2dn(role_dn)[0][0][1] + res.append(models.UserRoleAssociation( + id=self._create_ref(role_id, tenant_id, user_id), + user_id=user_id, + role_id=role_id, + tenant_id=tenant_id)) + return res + + def ref_get(self, id): + role_id, tenant_id, user_id = self._explode_ref(id) + user_dn = self.api.user._id_to_dn(user_id) + role_dn = self._subrole_id_to_dn(role_id, tenant_id) + query = '(&(objectClass=keystoneTenantRole)(member=%s))' % (user_dn,) + try: + res = search_s(role_dn, ldap.SCOPE_BASE, query) + except ldap.NO_SUCH_OBJECT: + return None + if len(res) == 0: + return None + return models.UserRoleAssociation(id=id, role_id=role_id, + tenant_id=tenant_id, user_id=user_id) + + def ref_delete(self, id): + role_id, tenant_id, user_id = self._explode_ref(id) + user_dn = self.api.user._id_to_dn(user_id) + role_dn = self._subrole_id_to_dn(role_id, tenant_id) + conn = self.api.get_connection() + try: + conn.modify_s(role_dn, [(ldap.MOD_DELETE, 'member', [user_dn])]) + except ldap.NO_SUCH_ATTRIBUTE: + raise exception.NotFound("No such user in role") + + def ref_get_page(self, marker, limit, user_id): + all_roles = self.ref_get_all_global_roles(user_id) + for tenant in self.api.tenant.get_all(): + all_roles += self.ref_get_all_tenant_roles(user_id, tenant.id) + return self._get_page(marker, limit, all_roles) + + def ref_get_page_markers(self, user_id, marker, limit): + all_roles = self.ref_get_all_global_roles(user_id) + for tenant in self.api.tenant.get_all(): + all_roles += self.ref_get_all_tenant_roles(user_id, tenant.id) + return self._get_page_markers(marker, limit, all_roles) diff --git a/keystone/backends/ldap/api/tenant.py b/keystone/backends/ldap/api/tenant.py new file mode 100644 index 0000000000..3b1204eace --- /dev/null +++ b/keystone/backends/ldap/api/tenant.py @@ -0,0 +1,53 @@ +import ldap + +from keystone.backends.api import BaseTenantAPI +from keystone.backends.sqlalchemy.api.tenant import TenantAPI as SQLTenantAPI + +from .. import models +from .base import BaseLdapAPI, add_redirects + +class TenantAPI(BaseLdapAPI, BaseTenantAPI): + DEFAULT_TREE_DN = 'ou=Groups,dc=example,dc=com' + options_name = 'tenant_tree_dn' + object_class = 'keystoneTenant' + model = models.Tenant + attribute_mapping = { 'desc': 'description' } + + def get_user_tenants(self, user_id): + user_dn = self.api.user._id_to_dn(user_id) + query = '(member=%s)' % (user_dn,) + return self.get_all(query) + + def tenants_for_user_get_page(self, user, marker, limit): + return self._get_page(marker, limit, self.get_user_tenants(user.id)) + + def tenants_for_user_get_page_markers(self, user, marker, limit): + return self._get_page_markers(marker, limit, + self.get_user_tenants(user.id)) + + def is_empty(self, id): + tenant = self._ldap_get(id) + empty = len(tenant[1].get('member', [])) == 0 + return empty and len(self.api.role.get_role_assignments(id)) == 0 + + def get_role_assignments(self, tenant_id): + return self.api.role.get_role_assignments(tenant_id) + + def add_user(self, tenant_id, user_id): + conn = self.api.get_connection() + conn.modify_s(self._id_to_dn(tenant_id), + [(ldap.MOD_ADD, 'member', self.api.user._id_to_dn(user_id))]) + + def remove_user(self, tenant_id, user_id): + conn = self.api.get_connection() + conn.modify_s(self._id_to_dn(tenant_id), + [(ldap.MOD_DELETE, 'member', self.api.user._id_to_dn(user_id))]) + + def get_users(self, tenant_id): + tenant = self._ldap_get(tenant_id) + res = [] + for user_dn in tenant[1].get('member',[]): + res.append(self.api.user.get(ldap.dn.str2dn(user_dn)[0][0][1])) + return res + + add_redirects(locals(), SQLTenantAPI, ['get_all_endpoints']) diff --git a/keystone/backends/ldap/api/user.py b/keystone/backends/ldap/api/user.py new file mode 100644 index 0000000000..cb9c82a276 --- /dev/null +++ b/keystone/backends/ldap/api/user.py @@ -0,0 +1,97 @@ +import ldap + +from keystone import utils +from keystone.backends.api import BaseUserAPI +from keystone.backends.sqlalchemy.api.user import UserAPI as SQLUserAPI + +from .. import models +from .base import BaseLdapAPI, add_redirects + +class UserAPI(BaseLdapAPI, BaseUserAPI): + DEFAULT_TREE_DN = 'ou=Users,dc=example,dc=com' + options_name = 'user_tree_dn' + object_class = 'keystoneUser' + model = models.User + attribute_mapping = { 'password': 'userPassword', 'email': 'mail' } + attribute_ignore = ['tenant_id'] + + def __check_and_use_hashed_password(self, values): + if type(values) is dict and 'password' in values.keys(): + values['password'] = utils.get_hashed_password(values['password']) + elif type(values) is models.User: + values.password = utils.get_hashed_password(values.password) + + def _ldap_res_to_model(self, res): + obj = super(UserAPI, self)._ldap_res_to_model(res) + tenants = self.api.tenant.get_user_tenants(obj.id) + if len(tenants) > 0: + obj.tenant_id = tenants[0].id + return obj + + def create(self, values): + self.__check_and_use_hashed_password(values) + super(UserAPI, self).create(values) + if values['tenant_id'] is not None: + self.api.tenant.add_user(values['tenant_id'], values['id']) + + def update(self, id, values): + old_obj = self.get(id) + try: + new_tenant = values['tenant_id'] + except KeyError: + pass + else: + if old_obj.tenant_id != new_tenant: + self.api.tenant.remove_user(old_obj.tenant_id, id) + self.api.tenant.add_user(new_tenant, id) + super(UserAPI, self).update(id, values, old_obj) + + def get_by_email(self, email): + users = self.get_all('(mail=%s)' % \ + (ldap.filter.escape_filter_chars(email),)) + try: + return users[0] + except IndexError: + return None + + def user_roles_by_tenant(self, user_id, tenant_id): + return self.api.role.ref_get_all_tenant_roles(user_id, tenant_id) + + def get_by_tenant(self, id, tenant_id): + user_dn = self._id_to_dn(id) + user = self.get(id) + tenant = self.api.tenant._ldap_get(tenant_id, + '(member=%s)' % (user_dn,)) + if tenant is not None: + return user + else: + return None + + def delete_tenant_user(self, id, tenant_id): + self.api.tenant.remove_user(tenant_id, id) + self.delete(id) + + def user_role_add(self, values): + return self.api.role.add_user(values.role_id, values.user_id, + values.tenant_id) + + def user_get_update(self, id): + return self.get(id) + + def users_get_page(self, marker, limit): + return self.get_page(marker, limit) + + def users_get_page_markers(self, marker, limit): + return self.get_page_markers(marker, limit) + + def users_get_by_tenant_get_page(self, tenant_id, marker, limit): + return self._get_page(marker, limit, + self.api.tenant.get_users(tenant_id)) + + def users_get_by_tenant_get_page_markers(self, tenant_id, marker, limit): + return self._get_page_markers(marker, limit, + self.api.tenant.get_users(tenant_id)) + + add_redirects(locals(), SQLUserAPI, ['get_by_group', 'tenant_group', + 'tenant_group_delete', 'user_groups_get_all', + 'users_tenant_group_get_page', 'users_tenant_group_get_page_markers']) diff --git a/keystone/backends/ldap/fakeldap.py b/keystone/backends/ldap/fakeldap.py new file mode 100644 index 0000000000..44d34e48b2 --- /dev/null +++ b/keystone/backends/ldap/fakeldap.py @@ -0,0 +1,278 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# 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. +"""Fake LDAP server for test harness. + +This class does very little error checking, and knows nothing about ldap +class definitions. It implements the minimum emulation of the python ldap +library to work with nova. + +""" + +import logging +import re +import shelve + +from ldap import (dn, filter, modlist, + SCOPE_BASE, SCOPE_ONELEVEL, SCOPE_SUBTREE, MOD_ADD, MOD_DELETE, MOD_REPLACE, + NO_SUCH_OBJECT, OBJECT_CLASS_VIOLATION, SERVER_DOWN, NO_SUCH_ATTRIBUTE, + ALREADY_EXISTS) + + +scope_names = { + SCOPE_BASE: 'SCOPE_BASE', + SCOPE_ONELEVEL: 'SCOPE_ONELEVEL', + SCOPE_SUBTREE: 'SCOPE_SUBTREE', +} + + +LOG = logging.getLogger('keystone.backends.ldap.fakeldap') + + +def initialize(uri): + """Opens a fake connection with an LDAP server.""" + return FakeLDAP(uri) + + +def _match_query(query, attrs): + """Match an ldap query to an attribute dictionary. + + The characters &, |, and ! are supported in the query. No syntax checking + is performed, so malformed querys will not work correctly. + """ + # cut off the parentheses + inner = query[1:-1] + if inner.startswith('&'): + # cut off the & + l, r = _paren_groups(inner[1:]) + return _match_query(l, attrs) and _match_query(r, attrs) + if inner.startswith('|'): + # cut off the | + l, r = _paren_groups(inner[1:]) + return _match_query(l, attrs) or _match_query(r, attrs) + if inner.startswith('!'): + # cut off the ! and the nested parentheses + return not _match_query(query[2:-1], attrs) + + (k, _sep, v) = inner.partition('=') + return _match(k, v, attrs) + + +def _paren_groups(source): + """Split a string into parenthesized groups.""" + count = 0 + start = 0 + result = [] + for pos in xrange(len(source)): + if source[pos] == '(': + if count == 0: + start = pos + count += 1 + if source[pos] == ')': + count -= 1 + if count == 0: + result.append(source[start:pos + 1]) + return result + + +def _match(key, value, attrs): + """Match a given key and value against an attribute list.""" + if key not in attrs: + return False + # This is a wild card search. Implemented as all or nothing for now. + if value == "*": + return True + if key != "objectclass": + return value in attrs[key] + # it is an objectclass check, so check subclasses + values = _subs(value) + for v in values: + if v in attrs[key]: + return True + return False + + +def _subs(value): + """Returns a list of subclass strings. + + The strings represent the ldap objectclass plus any subclasses that + inherit from it. Fakeldap doesn't know about the ldap object structure, + so subclasses need to be defined manually in the dictionary below. + + """ + subs = {'groupOfNames': ['keystoneTenant', 'keystoneRole', 'keystoneTenantRole']} + if value in subs: + return [value] + subs[value] + return [value] + + +server_fail = False + + +class FakeLDAP(object): + """Fake LDAP connection.""" + + def __init__(self, url): + LOG.debug("FakeLDAP initialize url=%s" % (url,)) + self.db = shelve.open(url[7:]) + + def simple_bind_s(self, dn, password): + """This method is ignored, but provided for compatibility.""" + if server_fail: + raise SERVER_DOWN + LOG.debug("FakeLDAP bind dn=%s" % (dn,)) + + def unbind_s(self): + """This method is ignored, but provided for compatibility.""" + if server_fail: + raise SERVER_DOWN + pass + + def add_s(self, dn, attrs): + """Add an object with the specified attributes at dn.""" + if server_fail: + raise SERVER_DOWN + + key = "%s%s" % (self.__prefix, dn) + LOG.debug("FakeLDAP add item: dn=%s, attrs=%s" % (dn, attrs)) + if self.db.has_key(key): + LOG.error("FakeLDAP add item failed: dn '%s' is already in store." % + (dn,)) + raise ALREADY_EXISTS + self.db[key] = dict([(k, v if isinstance(v, list) else [v]) + for k, v in attrs]) + self.db.sync() + + def delete_s(self, dn): + """Remove the ldap object at specified dn.""" + if server_fail: + raise SERVER_DOWN + + key = "%s%s" % (self.__prefix, dn) + LOG.debug("FakeLDAP delete item: dn=%s" % (dn,)) + try: + del self.db[key] + except KeyError: + LOG.error("FakeLDAP delete item failed: dn '%s' not found." % (dn,)) + raise NO_SUCH_OBJECT + self.db.sync() + + def modify_s(self, dn, attrs): + """Modify the object at dn using the attribute list. + + Args: + dn -- a dn + attrs -- a list of tuples in the following form: + ([MOD_ADD | MOD_DELETE | MOD_REPACE], attribute, value) + + """ + if server_fail: + raise SERVER_DOWN + + key = "%s%s" % (self.__prefix, dn) + LOG.debug("FakeLDAP modify item: dn=%s attrs=%s" % (dn, attrs)) + try: + entry = self.db[key] + except KeyError: + LOG.error("FakeLDAP modify item failed: dn '%s' not found." % (dn,)) + raise NO_SUCH_OBJECT + + for cmd, k, v in attrs: + values = entry.setdefault(k, []) + if cmd == MOD_ADD: + if isinstance(v, list): + values += v + else: + values.append(v) + elif cmd == MOD_REPLACE: + values[:] = v if isinstance(v, list) else [v] + elif cmd == MOD_DELETE: + if v is None: + if len(values) == 0: + LOG.error("FakeLDAP modify item failed: " + "item has no attribute '%s' to delete" % (k,)) + raise NO_SUCH_ATTRIBUTE + values[:] = [] + else: + if not isinstance(v,list): + v = [v] + for val in v: + try: + values.remove(val) + except ValueError: + LOG.error("FakeLDAP modify item failed: " + "item has no attribute '%s' with value '%s'" + " to delete" % (k, val)) + raise NO_SUCH_ATTRIBUTE + else: + LOG.error("FakeLDAP modify item failed: unknown command %s" % (cmd,)) + raise NotImplementedError( \ + "modify_s action %s not implemented" % (cmd,)) + self.db[key] = entry + self.db.sync() + + def search_s(self, dn, scope, query=None, fields=None): + """Search for all matching objects under dn using the query. + + Args: + dn -- dn to search under + scope -- only SCOPE_BASE and SCOPE_SUBTREE are supported + query -- query to filter objects by + fields -- fields to return. Returns all fields if not specified + + """ + if server_fail: + raise SERVER_DOWN + + LOG.debug("FakeLDAP search at dn=%s scope=%s query='%s'" % + (dn, scope_names.get(scope, scope), query)) + if scope == SCOPE_BASE: + try: + item_dict = self.db["%s%s" % (self.__prefix, dn)] + except KeyError: + LOG.debug("FakeLDAP search fail: dn not found for SCOPE_BASE") + raise NO_SUCH_OBJECT + results = [(dn, item_dict)] + elif scope == SCOPE_SUBTREE: + results = [(k[len(self.__prefix):], v) + for k, v in self.db.iteritems() + if re.match("%s.*,%s" % (self.__prefix, dn), k)] + elif scope == SCOPE_ONELEVEL: + results = [(k[len(self.__prefix):], v) + for k, v in self.db.iteritems() + if re.match("%s\w+=[^,]+,%s" % (self.__prefix, dn), k)] + else: + LOG.error("FakeLDAP search fail: unknown scope %s" % (scope,)) + raise NotImplementedError("Search scope %s not implemented." % + (scope,)) + + objects = [] + for dn, attrs in results: + # filter the objects by query + if not query or _match_query(query, attrs): + # filter the attributes by fields + attrs = dict([(k, v) for k, v in attrs.iteritems() + if not fields or k in fields]) + objects.append((dn, attrs)) + # pylint: enable=E1103 + LOG.debug("FakeLDAP search result: %s" % (objects,)) + return objects + + @property + def __prefix(self): # pylint: disable=R0201 + """Get the prefix to use for all keys.""" + return 'ldap:' diff --git a/keystone/backends/ldap/models.py b/keystone/backends/ldap/models.py new file mode 100644 index 0000000000..c2d86da330 --- /dev/null +++ b/keystone/backends/ldap/models.py @@ -0,0 +1,48 @@ +from collections import Mapping + +__all__ = ['UserRoleAssociation', 'Endpoints', 'Role', 'Tenant', 'User', + 'Credentials'] + + +def create_model(name, attrs): + class C(Mapping): + __slots__ = attrs + def __init__(self, arg=None, **kwargs): + if arg is None: + arg = kwargs + if isinstance(arg, dict): + missed_attrs = set(attrs) + for k, v in kwargs.iteritems(): + setattr(self, k, v) + missed_attrs.remove(k) + for name in missed_attrs: + setattr(self, name, None) + elif isinstance(arg, C): + for name in attrs: + setattr(self, name, getattr(arg, name)) + else: + raise ValueError + + def __getitem__(self, name): + return getattr(self, name) + + def __setitem__(self, name, value): + return setattr(self, name, value) + + def __iter__(self): + return iter(attrs) + + def __len__(self): + return len(attrs) + C.__name__ = name + return C + + +UserRoleAssociation = create_model('UserRoleAssociation', + ['id', 'user_id', 'role_id', 'tenant_id']) +Endpoints = create_model('Endpoints', ['tenant_id', 'endpoint_template_id']) #? +Role = create_model('Role', ['id', 'desc']) +Tenant = create_model('Tenant', ['id', 'desc', 'enabled']) +User = create_model('User', ['id', 'password', 'email', 'enabled', 'tenant_id']) +Credentials = create_model('Credentials', ['user_id', 'type', 'key', 'secret']) +