diff options
| author | Ziad Sawalha <github@highbridgellc.com> | 2011-07-19 18:08:06 -0500 |
|---|---|---|
| committer | Ziad Sawalha <github@highbridgellc.com> | 2011-07-19 18:08:06 -0500 |
| commit | 25380b0eb367fa2cb5fa5dbb075d6f4016ffc60f (patch) | |
| tree | 0cb9d345700ac58a531f44961182f2a5eac0742a | |
| parent | 6b9e4b4fbff741a672cbdc9e7f7feafc583cd7a0 (diff) | |
| parent | 39b944eefbc3a84b6277d8002d6a6a42289c4ffd (diff) | |
| download | keystone-25380b0eb367fa2cb5fa5dbb075d6f4016ffc60f.tar.gz keystone-25380b0eb367fa2cb5fa5dbb075d6f4016ffc60f.tar.xz keystone-25380b0eb367fa2cb5fa5dbb075d6f4016ffc60f.zip | |
Merge LDAP changes
| -rw-r--r-- | .gitignore | 1 | ||||
| -rwxr-xr-x | bin/keystone-manage | 69 | ||||
| -rwxr-xr-x | keystone/backends/api.py | 311 | ||||
| -rw-r--r-- | keystone/backends/ldap/api/__init__.py | 25 | ||||
| -rw-r--r-- | keystone/backends/ldap/api/base.py | 150 | ||||
| -rw-r--r-- | keystone/backends/ldap/api/role.py | 154 | ||||
| -rw-r--r-- | keystone/backends/ldap/api/tenant.py | 53 | ||||
| -rw-r--r-- | keystone/backends/ldap/api/user.py | 97 | ||||
| -rw-r--r-- | keystone/backends/ldap/fakeldap.py | 278 | ||||
| -rw-r--r-- | keystone/backends/ldap/models.py | 48 | ||||
| -rwxr-xr-x | keystone/backends/sqlalchemy/api/user.py | 3 | ||||
| -rw-r--r-- | keystone/frontends/legacy_token_auth.py | 2 | ||||
| -rwxr-xr-x | keystone/test/unit/test_common.py | 16 | ||||
| -rwxr-xr-x | keystone/utils.py | 8 |
14 files changed, 1018 insertions, 197 deletions
@@ -6,6 +6,7 @@ .pydevproject/ .settings/ keystone.db +ldap.db keystone.token.db .*.swp *.log diff --git a/bin/keystone-manage b/bin/keystone-manage index 2f3b0679..fde72d18 100755 --- a/bin/keystone-manage +++ b/bin/keystone-manage @@ -23,6 +23,7 @@ Keystone Identity Server - CLI Management Interface """ import datetime +import logging import optparse import os import sys @@ -40,7 +41,7 @@ import keystone from keystone.common import config import keystone.backends as db import keystone.backends.api as db_api -import keystone.backends.sqlalchemy.models as db_models +import keystone.backends.models as db_models def Main(): @@ -127,7 +128,7 @@ def Main(): db_api.user.create(object) print "SUCCESS: User %s created." % object.id except Exception as exc: - print "ERROR: Failed to create user %s: %s" % (object_id, exc) + raise Exception("Failed to create user %s" % (object_id,), sys.exc_info()) return elif command == "disable": try: @@ -138,7 +139,7 @@ def Main(): db_api.user.update(object_id, object) print "SUCCESS: User %s disabled." % object.id except Exception as exc: - print "ERROR: Failed to disable user %s: %s" % (object_id, exc) + raise Exception("Failed to disable user %s" % (object_id,), sys.exc_info()) return elif command == "list": try: @@ -160,7 +161,7 @@ def Main(): for row in objects: print row.id, row.enabled, row.tenant_id except Exception, e: - print 'Error getting all users:', str(e) + raise Exception("Error getting all users", sys.exc_info()) return elif object_type == "tenant": if command == "add": @@ -172,9 +173,7 @@ def Main(): print "SUCCESS: Tenant %s created." % object.id return except Exception as exc: - print "ERROR: Failed to create tenant %s: %s" % ( - object_id, exc) - return + raise Exception("Failed to create tenant %s" % (object_id,), sys.exc_info()) elif command == "list": try: objects = db_api.tenant.get_all() @@ -184,8 +183,8 @@ def Main(): print '-' * 20 for row in objects: print row.id, row.enabled - except Exception, e: - print 'Error getting all users: %s', str(e) + except Exception, exc: + raise Exception("Error getting all tenants", sys.exc_info()) return elif command == "disable": try: @@ -196,8 +195,7 @@ def Main(): db_api.tenant.update(object_id, object) print "SUCCESS: Tenant %s disabled." % object.id except Exception as exc: - print "ERROR: Failed to disable tenant %s: %s" % ( - object_id, exc) + raise Exception("Failed to disable tenant %s" % (object_id,), sys.exc_info()) return elif object_type == "role": if command == "add": @@ -208,8 +206,7 @@ def Main(): print "SUCCESS: Role %s created successfully." % object.id return except Exception as exc: - print "ERROR: Failed to create role %s: %s" % (object_id, exc) - return + raise Exception("Failed to create role %s" % (object_id,), sys.exc_info()) elif command == "list": if len(args) == 3: tenant = args[2] @@ -223,8 +220,9 @@ def Main(): for row in objects: print row.user_id, row.role_id except Exception, e: - print 'Error getting all role assignments for %s:' % \ - tenant, str(e) + raise Exception( \ + "Error getting all role assignments for %s" % (tenant,), + sys.exc_info()) return else: tenant = None @@ -238,7 +236,7 @@ def Main(): for row in objects: print row.id except Exception, e: - print 'Error getting all roles:', str(e) + raise Exception("Error getting all roles", sys.exc_info()) return elif command == "grant": if len(args) < 4: @@ -259,8 +257,8 @@ def Main(): print "SUCCESS: Granted %s the %s role on %s." % \ (object.user_id, object.role_id, object.tenant_id) except Exception as exc: - print "ERROR: Failed to grant role %s to %s on %s: %s" % ( - object_id, user, tenant, exc) + raise Exception("Failed to grant role %s to %s on %s" % \ + (object_id, user, tenant), sys.exc_info()) return elif object_type == "endpointTemplates": if command == "add": @@ -290,9 +288,8 @@ def Main(): "to %s." % (object.service, object.public_url) return except Exception as exc: - print "ERROR: Failed to create EndpointTemplates for " \ - "%s: %s" % (service, exc) - return + raise Exception("Failed to create EndpointTemplates for %s" % \ + (service,), sys.exc_info()) elif command == "list": if len(args) == 3: tenant = args[2] @@ -307,8 +304,8 @@ def Main(): for row in objects: print row.service, row.region, row.public_url except Exception, e: - print 'Error getting all endpoints for %s:' % \ - tenant, str(e) + raise Exception("Error getting all endpoints for %s" % \ + (tenant,), sys.exc_info()) return else: tenant = None @@ -322,7 +319,7 @@ def Main(): for row in objects: print row.service, row.region, row.public_url except Exception, e: - print 'Error getting all EndpointTemplates:', str(e) + raise Exception("Error getting all EndpointTemplates", sys.exc_info()) return elif object_type == "endpoint": if command == "add": @@ -341,8 +338,7 @@ def Main(): (endpoint_template_id, tenant_id) return except Exception as exc: - print "ERROR: Failed to create Endpoint: %s" % exc - return + raise Exception("Failed to create Endpoint", sys.exc_info()) elif object_type == "token": if command == "add": if len(args) < 6: @@ -361,8 +357,7 @@ def Main(): print "SUCCESS: Token %s created." % object.id return except Exception as exc: - print "ERROR: Failed to create token %s: %s" % (object_id, exc) - return + raise Exception("Failed to create token %s" % (object_id,), sys.exc_info()) elif command == "list": try: objects = db_api.token.get_all() @@ -373,7 +368,7 @@ def Main(): for row in objects: print row.id, row.user_id, row.expires, row.tenant_id except Exception, e: - print 'Error getting all tokens:', str(e) + raise Exception("Error getting all tokens", sys.exc_info()) return elif command == "delete": try: @@ -384,12 +379,22 @@ def Main(): db_api.token.delete(object_id) print 'SUCCESS: Token %s deleted.' % object_id except Exception, e: - print 'ERROR: Failed to delete token %s: %s' % \ - object_id, str(e) + raise Exception("Failed to delete token %s" % (object_id,), sys.exc_info()) return # Command not handled print ("ERROR: %s %s not yet supported" % (object_type, command)) if __name__ == '__main__': - Main() + try: + Main() + except Exception as exc: + try: + info = exc.args[1] + except IndexError: + print "ERROR: %s" % (exc,) + logging.error(str(exc)) + else: + print "ERROR: %s: %s" % (exc.args[0], info[1].message) + logging.error(exc.args[0], exc_info=info) + sys.exit(1) diff --git a/keystone/backends/api.py b/keystone/backends/api.py index c75dff11..65dc286f 100755 --- a/keystone/backends/api.py +++ b/keystone/backends/api.py @@ -15,264 +15,287 @@ #API #TODO(Yogi) Refactor all API to separate classes specific to models. +# Function to dynamically set module references. +def set_value(variable_name, value): + if variable_name == 'endpoint_template': + global endpoint_template + endpoint_template = value + elif variable_name == 'group': + global group + group = value + elif variable_name == 'role': + global role + role = value + elif variable_name == 'tenant_group': + global tenant_group + tenant_group = value + elif variable_name == 'tenant': + global tenant + tenant = value + elif variable_name == 'token': + global token + token = value + elif variable_name == 'user': + global user + user = value + #Base APIs class BaseUserAPI(object): def get_all(self): - pass + raise NotImplementedError def get_by_group(self, user_id, group_id): - pass + raise NotImplementedError def tenant_group(self, values): - pass + raise NotImplementedError def tenant_group_delete(self, id, group_id): - pass + raise NotImplementedError def create(self, values): - pass + raise NotImplementedError def get(self, id): - pass + raise NotImplementedError def get_page(self, marker, limit): - pass + raise NotImplementedError def get_page_markers(self, marker, limit): - pass + raise NotImplementedError def get_by_email(self, email): - pass + raise NotImplementedError def get_groups(self, id): - pass + raise NotImplementedError def user_roles_by_tenant(self, user_id, tenant_id): - pass + raise NotImplementedError def update(self, id, values): - pass + raise NotImplementedError - def users_tenant_group_get_page(self, group_id, marker): - pass + def users_tenant_group_get_page(self, group_id, marker, limit): + raise NotImplementedError def users_tenant_group_get_page_markers(self, group_id, marker, limit): - pass + raise NotImplementedError def delete(self, id): - pass + raise NotImplementedError def get_by_tenant(self, id, tenant_id): - pass + raise NotImplementedError def get_group_by_tenant(self, id): - pass + raise NotImplementedError def delete_tenant_user(self, id, tenant_id): - pass + raise NotImplementedError def users_get_by_tenant(self, user_id, tenant_id): - pass + raise NotImplementedError def user_role_add(self, values): - pass + raise NotImplementedError def user_get_update(self, id): - pass + raise NotImplementedError def users_get_page(self, marker, limit): - pass + raise NotImplementedError def users_get_page_markers(self, marker, limit): - pass + raise NotImplementedError def users_get_by_tenant_get_page(self, tenant_id, marker, limit): - pass + raise NotImplementedError def users_get_by_tenant_get_page_markers(self, tenant_id, marker, limit): - pass + raise NotImplementedError def user_groups_get_all(self, user_id): - pass + raise NotImplementedError class BaseTokenAPI(object): def create(self, values): - pass - + raise NotImplementedError + def get(self, id): - pass - + raise NotImplementedError + def delete(self, id): - pass - + raise NotImplementedError + def get_for_user(self, user_id): - pass - + raise NotImplementedError + def get_for_user_by_tenant(self, user_id, tenant_id): - pass + raise NotImplementedError def get_all(self): - pass - + raise NotImplementedError + class BaseTenantGroupAPI(object): def create(self, values): - pass - + raise NotImplementedError + def is_empty(self, id): - pass - + raise NotImplementedError + def get(self, id, tenant): - pass - + raise NotImplementedError + def get_page(self, tenantId, marker, limit): - pass - + raise NotImplementedError + def get_page_markers(self, tenantId, marker, limit): - pass - + raise NotImplementedError + def update(self, id, tenant_id, values): - pass - + raise NotImplementedError + def delete(self, id, tenant_id): - pass - + raise NotImplementedError + class BaseTenantAPI(object): - def create(self, values): - pass - + raise NotImplementedError + def get(self, id): - pass - + raise NotImplementedError + def get_all(self): - pass - + raise NotImplementedError + def tenants_for_user_get_page(self, user, marker, limit): - pass - + raise NotImplementedError + def tenants_for_user_get_page_markers(self, user, marker, limit): - pass - + raise NotImplementedError + def get_page(self, marker, limit): - pass - + raise NotImplementedError + def get_page_markers(self, marker, limit): - pass - + raise NotImplementedError + def is_empty(self, id): - pass - + raise NotImplementedError + def update(self, id, values): - pass - + raise NotImplementedError + def delete(self, id): - pass - + raise NotImplementedError + def get_all_endpoints(self, tenant_id): - pass + raise NotImplementedError def get_role_assignments(self, tenant_id): - pass + raise NotImplementedError class BaseRoleAPI(object): def create(self, values): - pass - + raise NotImplementedError + def get(self, id): - pass - + raise NotImplementedError + def get_all(self): - pass - + raise NotImplementedError + def get_page(self, marker, limit): - pass - + raise NotImplementedError + def ref_get_page(self, marker, limit, user_id): - pass - + raise NotImplementedError + def ref_get_all_global_roles(self, user_id): - pass - + raise NotImplementedError + def ref_get_all_tenant_roles(self, user_id, tenant_id): - pass - + raise NotImplementedError + def ref_get(self, id): - pass - + raise NotImplementedError + def ref_delete(self, id): - pass - + raise NotImplementedError + def get_page_markers(self, marker, limit): - pass + raise NotImplementedError def ref_get_page_markers(self, user_id, marker, limit): - pass + raise NotImplementedError class BaseGroupAPI(object): def get(self, id): - pass - + raise NotImplementedError + def get_users(self, id): - pass - + raise NotImplementedError + def get_all(self): - pass - + raise NotImplementedError + def get_page(self, marker, limit): - pass - + raise NotImplementedError + def get_page_markers(self, marker, limit): - pass - + raise NotImplementedError + def delete(self, id): - pass - + raise NotImplementedError + def get_by_user_get_page(self, user_id, marker, limit): - pass - + raise NotImplementedError + def get_by_user_get_page_markers(self, user_id, marker, limit): - pass - + raise NotImplementedError + class BaseEndpointTemplateAPI(object): def create(self, values): - pass - + raise NotImplementedError + def get(self, id): - pass - + raise NotImplementedError + def get_all(self): - pass - + raise NotImplementedError + def get_page(self, marker, limit): - pass - + raise NotImplementedError + def get_page_markers(self, marker, limit): - pass - + raise NotImplementedError + def endpoint_get_by_tenant_get_page(self, tenant_id, marker, limit): - pass - + raise NotImplementedError + def endpoint_get_by_tenant_get_page_markers(self, tenant_id, marker, limit): - pass - + raise NotImplementedError + def endpoint_add(self, values): - pass - + raise NotImplementedError + def endpoint_get(self, id): - pass - + raise NotImplementedError + def endpoint_get_by_tenant(self, tenant_id): - pass - + raise NotImplementedError + def endpoint_delete(self, id): - pass + raise NotImplementedError endpoint_template = BaseEndpointTemplateAPI() group = BaseGroupAPI() @@ -281,27 +304,3 @@ tenant_group = BaseTenantGroupAPI() tenant = BaseTenantAPI() token = BaseTokenAPI() user = BaseUserAPI() - -# Function to dynamically set module references. -def set_value(variable_name, value): - if variable_name == 'endpoint_template': - global endpoint_template - endpoint_template = value - elif variable_name == 'group': - global group - group = value - elif variable_name == 'role': - global role - role = value - elif variable_name == 'tenant_group': - global tenant_group - tenant_group = value - elif variable_name == 'tenant': - global tenant - tenant = value - elif variable_name == 'token': - global token - token = value - elif variable_name == 'user': - global user - user = value 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']) + diff --git a/keystone/backends/sqlalchemy/api/user.py b/keystone/backends/sqlalchemy/api/user.py index 304ba204..891161f4 100755 --- a/keystone/backends/sqlalchemy/api/user.py +++ b/keystone/backends/sqlalchemy/api/user.py @@ -47,7 +47,8 @@ class UserAPI(BaseUserAPI): session = get_session() with session.begin(): usertenantgroup_ref = self.get_by_group(id, group_id, session) - session.delete(usertenantgroup_ref) + if usertenantgroup_ref is not None: + session.delete(usertenantgroup_ref) def create(self, values): diff --git a/keystone/frontends/legacy_token_auth.py b/keystone/frontends/legacy_token_auth.py index 02c413b3..1d7cfc64 100644 --- a/keystone/frontends/legacy_token_auth.py +++ b/keystone/frontends/legacy_token_auth.py @@ -72,9 +72,9 @@ class AuthProtocol(object): "password": utils.get_auth_key(self.request)}} #Make request to keystone new_request = Request.blank('/v2.0/tokens') + new_request.method = 'POST' new_request.headers['Content-type'] = 'application/json' new_request.accept = 'text/json' - new_request.method = 'POST' new_request.body = json.dumps(params) response = new_request.get_response(self.app) #Handle failures. diff --git a/keystone/test/unit/test_common.py b/keystone/test/unit/test_common.py index 706d59f7..819e35fb 100755 --- a/keystone/test/unit/test_common.py +++ b/keystone/test/unit/test_common.py @@ -817,12 +817,16 @@ def delete_all_endpoint(tenant_id, auth_token): #verify content obj = json.loads(content) - endpoints = obj["endpoints"]["values"] - for endpoint in endpoints: - url = '%stenants/%s/endpoints/%s' % (URL, tenant_id, endpoint["id"]) - header.request(url, "DELETE", body='', - headers={"Content-Type": "application/json", - "X-Auth-Token": str(auth_token)}) + try: + endpoints = obj["endpoints"]["values"] + except KeyError: + pass + else: + for endpoint in endpoints: + url = '%stenants/%s/endpoints/%s' % (URL, tenant_id, endpoint["id"]) + header.request(url, "DELETE", body='', + headers={"Content-Type": "application/json", + "X-Auth-Token": str(auth_token)}) if __name__ == '__main__': unittest.main() diff --git a/keystone/utils.py b/keystone/utils.py index 321381ff..176affc2 100755 --- a/keystone/utils.py +++ b/keystone/utils.py @@ -151,7 +151,13 @@ def import_module(module_name, class_name=None): module and options. If no class_name is given, it is assumed to be the last part of the module_name string.''' if class_name is None: - module_name, _separator, class_name = module_name.rpartition('.') + try: + __import__(module_name) + return sys.modules[module_name] + except ImportError as exc: + module_name, _separator, class_name = module_name.rpartition('.') + if not exc.message.startswith('No module named %s' % (class_name,)): + raise try: __import__(module_name) return getattr(sys.modules[module_name], class_name) |
