summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorYuriy Taraday <yorik.sar@gmail.com>2011-07-18 20:04:23 +0400
committerYuriy Taraday <yorik.sar@gmail.com>2011-07-18 20:04:23 +0400
commit39b944eefbc3a84b6277d8002d6a6a42289c4ffd (patch)
treede11c557abb3956f8ca04a5e31cd172389727856
parent662d24575872265430abe039b748429cd668347c (diff)
Add first implementation of LDAP backend.
-rw-r--r--.gitignore1
-rw-r--r--keystone/backends/ldap/__init__.py16
-rw-r--r--keystone/backends/ldap/api/__init__.py25
-rw-r--r--keystone/backends/ldap/api/base.py150
-rw-r--r--keystone/backends/ldap/api/role.py154
-rw-r--r--keystone/backends/ldap/api/tenant.py53
-rw-r--r--keystone/backends/ldap/api/user.py97
-rw-r--r--keystone/backends/ldap/fakeldap.py278
-rw-r--r--keystone/backends/ldap/models.py48
9 files changed, 822 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index c4dff7bb..a08c9b54 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 00000000..42766600
--- /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 00000000..9244fdbb
--- /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 00000000..0018b436
--- /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 00000000..b1bd7661
--- /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 00000000..3b1204ea
--- /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 00000000..cb9c82a2
--- /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 00000000..44d34e48
--- /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 00000000..c2d86da3
--- /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'])
+