From e159e829c6357bd2160a4b1fdcd3c62e8f8d9507 Mon Sep 17 00:00:00 2001 From: Yuriy Taraday Date: Mon, 18 Jul 2011 19:48:35 +0400 Subject: Add exception throwing and logging to keystone-manage. --- bin/keystone-manage | 64 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/bin/keystone-manage b/bin/keystone-manage index 0475b99b..1ed65cbd 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 @@ -126,7 +127,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: @@ -137,7 +138,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: @@ -159,7 +160,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": @@ -171,8 +172,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() @@ -182,8 +182,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: @@ -194,7 +194,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": @@ -205,8 +205,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] @@ -220,8 +219,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 @@ -235,7 +235,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: @@ -256,7 +256,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": @@ -285,9 +286,8 @@ def Main(): (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] @@ -301,8 +301,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 @@ -316,7 +316,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": @@ -335,8 +335,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: @@ -355,8 +354,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() @@ -367,7 +365,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: @@ -378,8 +376,7 @@ 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 @@ -387,4 +384,15 @@ def Main(): 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) -- cgit From 2bb3ebf5f02ff05b1c920e0fbb33e3d3d77e8cd4 Mon Sep 17 00:00:00 2001 From: Yuriy Taraday Date: Mon, 18 Jul 2011 19:51:25 +0400 Subject: Removed hardcoded references to sql backends. --- bin/keystone-manage | 2 +- keystone/server.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/bin/keystone-manage b/bin/keystone-manage index 1ed65cbd..cc74a5c3 100755 --- a/bin/keystone-manage +++ b/bin/keystone-manage @@ -41,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(): diff --git a/keystone/server.py b/keystone/server.py index 79ed98af..115fa2a1 100755 --- a/keystone/server.py +++ b/keystone/server.py @@ -50,8 +50,6 @@ if os.path.exists(os.path.join(POSSIBLE_TOPDIR, 'keystone', '__init__.py')): from keystone.common import wsgi import keystone.backends as db -import keystone.backends.sqlalchemy -import keystone.backends.alterdb import keystone.logic.service as serv import keystone.logic.types.tenant as tenants import keystone.logic.types.role as roles -- cgit From e01b61046275d4a04d47e23c75afe8b01845f0c1 Mon Sep 17 00:00:00 2001 From: Yuriy Taraday Date: Mon, 18 Jul 2011 19:53:56 +0400 Subject: Tweaked import_module to clearly import module if it can. --- keystone/utils.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/keystone/utils.py b/keystone/utils.py index ddd63760..c1e9ba0d 100755 --- a/keystone/utils.py +++ b/keystone/utils.py @@ -175,10 +175,16 @@ 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) except (ImportError, ValueError, AttributeError), exception: raise ImportError(_('Class %s.%s cannot be found (%s)') % - (module_name, class_name, exception)) \ No newline at end of file + (module_name, class_name, exception)) -- cgit From 18ad51c566b4d18faf0710b9b7e606d5113cca08 Mon Sep 17 00:00:00 2001 From: Yuriy Taraday Date: Mon, 18 Jul 2011 19:55:25 +0400 Subject: Add check to sqlalchemy backed to prevent loud crush. --- keystone/backends/sqlalchemy/api/user.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/keystone/backends/sqlalchemy/api/user.py b/keystone/backends/sqlalchemy/api/user.py index fcdc74f7..53eb73eb 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): @@ -431,4 +432,4 @@ class UserAPI(BaseUserAPI): group.id).all() def get(): - return UserAPI() \ No newline at end of file + return UserAPI() -- cgit From decdad2c1849d721062ef35930604f9ceb5f3c11 Mon Sep 17 00:00:00 2001 From: Yuriy Taraday Date: Mon, 18 Jul 2011 19:56:08 +0400 Subject: Fixed bug causing request body setting to fail. --- keystone/frontends/legacy_token_auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keystone/frontends/legacy_token_auth.py b/keystone/frontends/legacy_token_auth.py index 647a5697..187a8bce 100644 --- a/keystone/frontends/legacy_token_auth.py +++ b/keystone/frontends/legacy_token_auth.py @@ -69,10 +69,10 @@ 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.body = json.dumps(params) - new_request.method = 'POST' response = new_request.get_response(self.app) #Handle failures. if not str(response.status).startswith('20'): -- cgit From 2a20fa36afa742200c34620c58db597c6a581452 Mon Sep 17 00:00:00 2001 From: Yuriy Taraday Date: Mon, 18 Jul 2011 19:57:04 +0400 Subject: Made delete_all_endpoint calm if there is nothing to do. --- keystone/test/unit/test_common.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) 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() -- cgit From 662d24575872265430abe039b748429cd668347c Mon Sep 17 00:00:00 2001 From: Yuriy Taraday Date: Mon, 18 Jul 2011 19:58:00 +0400 Subject: Made all API methods raise NotImplementedError if they are not implemented in backend. --- keystone/backends/api.py | 164 +++++++++++++++++++++++------------------------ 1 file changed, 82 insertions(+), 82 deletions(-) diff --git a/keystone/backends/api.py b/keystone/backends/api.py index 11b07953..5e33f0f6 100755 --- a/keystone/backends/api.py +++ b/keystone/backends/api.py @@ -51,258 +51,258 @@ def set_value(variable_name, 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 -- cgit From 39b944eefbc3a84b6277d8002d6a6a42289c4ffd Mon Sep 17 00:00:00 2001 From: Yuriy Taraday Date: Mon, 18 Jul 2011 20:04:23 +0400 Subject: Add first implementation of LDAP backend. --- .gitignore | 1 + keystone/backends/ldap/__init__.py | 16 ++ keystone/backends/ldap/api/__init__.py | 25 +++ keystone/backends/ldap/api/base.py | 150 ++++++++++++++++++ keystone/backends/ldap/api/role.py | 154 ++++++++++++++++++ keystone/backends/ldap/api/tenant.py | 53 +++++++ keystone/backends/ldap/api/user.py | 97 ++++++++++++ keystone/backends/ldap/fakeldap.py | 278 +++++++++++++++++++++++++++++++++ keystone/backends/ldap/models.py | 48 ++++++ 9 files changed, 822 insertions(+) create mode 100644 keystone/backends/ldap/__init__.py create mode 100644 keystone/backends/ldap/api/__init__.py create mode 100644 keystone/backends/ldap/api/base.py create mode 100644 keystone/backends/ldap/api/role.py create mode 100644 keystone/backends/ldap/api/tenant.py create mode 100644 keystone/backends/ldap/api/user.py create mode 100644 keystone/backends/ldap/fakeldap.py create mode 100644 keystone/backends/ldap/models.py 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']) + -- cgit