Add first implementation of LDAP backend.
This commit is contained in:
parent
662d245758
commit
39b944eefb
1
.gitignore
vendored
1
.gitignore
vendored
@ -6,6 +6,7 @@
|
||||
.pydevproject/
|
||||
.settings/
|
||||
keystone.db
|
||||
ldap.db
|
||||
keystone.token.db
|
||||
.*.swp
|
||||
*.log
|
||||
|
16
keystone/backends/ldap/__init__.py
Normal file
16
keystone/backends/ldap/__init__.py
Normal file
@ -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))
|
25
keystone/backends/ldap/api/__init__.py
Normal file
25
keystone/backends/ldap/api/__init__.py
Normal file
@ -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
|
150
keystone/backends/ldap/api/base.py
Normal file
150
keystone/backends/ldap/api/base.py
Normal file
@ -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))
|
154
keystone/backends/ldap/api/role.py
Normal file
154
keystone/backends/ldap/api/role.py
Normal file
@ -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)
|
53
keystone/backends/ldap/api/tenant.py
Normal file
53
keystone/backends/ldap/api/tenant.py
Normal file
@ -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'])
|
97
keystone/backends/ldap/api/user.py
Normal file
97
keystone/backends/ldap/api/user.py
Normal file
@ -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'])
|
278
keystone/backends/ldap/fakeldap.py
Normal file
278
keystone/backends/ldap/fakeldap.py
Normal file
@ -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:'
|
48
keystone/backends/ldap/models.py
Normal file
48
keystone/backends/ldap/models.py
Normal file
@ -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'])
|
||||
|
Loading…
Reference in New Issue
Block a user