diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | doc/source/man/keystone-all.rst | 2 | ||||
-rw-r--r-- | doc/source/man/keystone-manage.rst | 2 | ||||
-rw-r--r-- | keystone/assignment/backends/kvs.py | 2 | ||||
-rw-r--r-- | keystone/assignment/backends/ldap.py | 2 | ||||
-rw-r--r-- | keystone/assignment/backends/sql.py | 27 | ||||
-rw-r--r-- | keystone/assignment/core.py | 15 | ||||
-rw-r--r-- | keystone/common/dependency.py | 33 | ||||
-rw-r--r-- | keystone/common/logging.py | 4 | ||||
-rw-r--r-- | keystone/common/wsgi.py | 2 | ||||
-rw-r--r-- | keystone/identity/backends/ldap.py | 10 | ||||
-rw-r--r-- | keystone/identity/backends/sql.py | 2 | ||||
-rw-r--r-- | keystone/identity/controllers.py | 10 | ||||
-rw-r--r-- | keystone/identity/core.py | 8 | ||||
-rw-r--r-- | keystone/service.py | 13 | ||||
-rw-r--r-- | keystone/test.py | 8 | ||||
-rw-r--r-- | keystone/trust/controllers.py | 18 | ||||
-rw-r--r-- | tests/test_auth.py | 4 | ||||
-rw-r--r-- | tests/test_backend_sql.py | 2 | ||||
-rw-r--r-- | tests/test_injection.py | 27 | ||||
-rw-r--r-- | tests/test_v3_identity.py | 20 | ||||
-rw-r--r-- | tests/test_wsgi.py | 4 |
22 files changed, 172 insertions, 44 deletions
@@ -1,5 +1,6 @@ *.pyc *.swp +*.egg/ vendor .ksl-venv .venv diff --git a/doc/source/man/keystone-all.rst b/doc/source/man/keystone-all.rst index 9b0859d9..76d5ab1f 100644 --- a/doc/source/man/keystone-all.rst +++ b/doc/source/man/keystone-all.rst @@ -6,7 +6,7 @@ keystone-all Keystone Startup Command ------------------------ -:Author: openstack@lists.launchpad.net +:Author: openstack@lists.openstack.org :Date: 2010-11-16 :Copyright: OpenStack LLC :Version: 2012.1 diff --git a/doc/source/man/keystone-manage.rst b/doc/source/man/keystone-manage.rst index 84a3ec9f..1da4b40a 100644 --- a/doc/source/man/keystone-manage.rst +++ b/doc/source/man/keystone-manage.rst @@ -6,7 +6,7 @@ keystone-manage Keystone Management Utility --------------------------- -:Author: openstack@lists.launchpad.net +:Author: openstack@lists.openstack.org :Date: 2010-11-16 :Copyright: OpenStack LLC :Version: 2012.1 diff --git a/keystone/assignment/backends/kvs.py b/keystone/assignment/backends/kvs.py index 4dfd908f..30d7b2eb 100644 --- a/keystone/assignment/backends/kvs.py +++ b/keystone/assignment/backends/kvs.py @@ -16,11 +16,13 @@ from keystone import assignment from keystone import clean +from keystone.common import dependency from keystone.common import kvs from keystone import exception from keystone import identity +@dependency.requires('identity_api') class Assignment(kvs.Base, assignment.Driver): def __init__(self): super(Assignment, self).__init__() diff --git a/keystone/assignment/backends/ldap.py b/keystone/assignment/backends/ldap.py index f8c81eae..9b273e40 100644 --- a/keystone/assignment/backends/ldap.py +++ b/keystone/assignment/backends/ldap.py @@ -21,6 +21,7 @@ import ldap as ldap from keystone import assignment from keystone import clean +from keystone.common import dependency from keystone.common import ldap as common_ldap from keystone.common import logging from keystone.common import models @@ -39,6 +40,7 @@ DEFAULT_DOMAIN = { } +@dependency.requires('identity_api') class Assignment(assignment.Driver): def __init__(self): super(Assignment, self).__init__() diff --git a/keystone/assignment/backends/sql.py b/keystone/assignment/backends/sql.py index 5ec435ff..cd2a0a5c 100644 --- a/keystone/assignment/backends/sql.py +++ b/keystone/assignment/backends/sql.py @@ -16,15 +16,14 @@ from keystone import assignment from keystone import clean +from keystone.common import dependency from keystone.common import sql from keystone.common.sql import migration from keystone import exception +@dependency.requires('identity_api') class Assignment(sql.Base, assignment.Driver): - def __init__(self): - super(Assignment, self).__init__() - self.identity_api = None # Internal interface to manage the database def db_sync(self, version=None): @@ -657,9 +656,10 @@ class Domain(sql.ModelBase, sql.DictBase): __tablename__ = 'domain' attributes = ['id', 'name', 'enabled'] id = sql.Column(sql.String(64), primary_key=True) - name = sql.Column(sql.String(64), unique=True, nullable=False) - enabled = sql.Column(sql.Boolean, default=True) + name = sql.Column(sql.String(64), nullable=False) + enabled = sql.Column(sql.Boolean, default=True, nullable=False) extra = sql.Column(sql.JsonBlob()) + __table_args__ = (sql.UniqueConstraint('name'), {}) class Project(sql.ModelBase, sql.DictBase): @@ -681,8 +681,9 @@ class Role(sql.ModelBase, sql.DictBase): __tablename__ = 'role' attributes = ['id', 'name'] id = sql.Column(sql.String(64), primary_key=True) - name = sql.Column(sql.String(64), unique=True, nullable=False) + name = sql.Column(sql.String(255), nullable=False) extra = sql.Column(sql.JsonBlob()) + __table_args__ = (sql.UniqueConstraint('name'), {}) class BaseGrant(sql.DictBase): @@ -720,9 +721,8 @@ class BaseGrant(sql.DictBase): class UserProjectGrant(sql.ModelBase, BaseGrant): __tablename__ = 'user_project_metadata' - user_id = sql.Column(sql.String(64), - primary_key=True) - project_id = sql.Column(sql.String(64), + user_id = sql.Column(sql.String(64), primary_key=True) + project_id = sql.Column(sql.String(64), sql.ForeignKey('project.id'), primary_key=True) data = sql.Column(sql.JsonBlob()) @@ -730,19 +730,22 @@ class UserProjectGrant(sql.ModelBase, BaseGrant): class UserDomainGrant(sql.ModelBase, BaseGrant): __tablename__ = 'user_domain_metadata' user_id = sql.Column(sql.String(64), primary_key=True) - domain_id = sql.Column(sql.String(64), primary_key=True) + domain_id = sql.Column(sql.String(64), sql.ForeignKey('domain.id'), + primary_key=True) data = sql.Column(sql.JsonBlob()) class GroupProjectGrant(sql.ModelBase, BaseGrant): __tablename__ = 'group_project_metadata' group_id = sql.Column(sql.String(64), primary_key=True) - project_id = sql.Column(sql.String(64), primary_key=True) + project_id = sql.Column(sql.String(64), sql.ForeignKey('project.id'), + primary_key=True) data = sql.Column(sql.JsonBlob()) class GroupDomainGrant(sql.ModelBase, BaseGrant): __tablename__ = 'group_domain_metadata' group_id = sql.Column(sql.String(64), primary_key=True) - domain_id = sql.Column(sql.String(64), primary_key=True) + domain_id = sql.Column(sql.String(64), sql.ForeignKey('domain.id'), + primary_key=True) data = sql.Column(sql.JsonBlob()) diff --git a/keystone/assignment/core.py b/keystone/assignment/core.py index b71e2a18..64edb3fa 100644 --- a/keystone/assignment/core.py +++ b/keystone/assignment/core.py @@ -35,6 +35,7 @@ DEFAULT_DOMAIN = {'description': @dependency.provider('assignment_api') +@dependency.requires('identity_api') class Manager(manager.Manager): """Default pivot point for the Assignment backend. @@ -45,18 +46,14 @@ class Manager(manager.Manager): api object by both managers. """ - def __init__(self, identity_api=None): - if identity_api is None: - from keystone import identity - identity_api = identity.Manager(self) - + def __init__(self): assignment_driver = CONF.assignment.driver + if assignment_driver is None: - assignment_driver = identity_api.default_assignment_driver() + identity_driver = dependency.REGISTRY['identity_api'].driver + assignment_driver = identity_driver.default_assignment_driver() + super(Manager, self).__init__(assignment_driver) - self.driver.identity_api = identity_api - self.identity_api = identity_api - self.identity_api.assignment_api = self def get_roles_for_user_and_project(self, user_id, tenant_id): """Get the roles associated with a user within given project. diff --git a/keystone/common/dependency.py b/keystone/common/dependency.py index 3ed261cc..a640031d 100644 --- a/keystone/common/dependency.py +++ b/keystone/common/dependency.py @@ -16,6 +16,8 @@ REGISTRY = {} +_future_dependencies = {} + class UnresolvableDependencyException(Exception): def __init__(self, name): @@ -32,6 +34,8 @@ def provider(name): init(self, *args, **kwargs) REGISTRY[name] = self + resolve_future_dependencies(name) + return __wrapped_init__ cls.__init__ = wrapped(cls.__init__) @@ -48,7 +52,13 @@ def requires(*dependencies): for dependency in self._dependencies: if dependency not in REGISTRY: - raise UnresolvableDependencyException(dependency) + if dependency in _future_dependencies: + _future_dependencies[dependency] += [self] + else: + _future_dependencies[dependency] = [self] + + continue + setattr(self, dependency, REGISTRY[dependency]) def wrapped(cls): @@ -67,6 +77,26 @@ def requires(*dependencies): return wrapped +def resolve_future_dependencies(provider_name=None): + if provider_name: + targets = _future_dependencies.pop(provider_name, []) + + for target in targets: + setattr(target, provider_name, REGISTRY[provider_name]) + + return + + try: + for dependency, targets in _future_dependencies.iteritems(): + if dependency not in REGISTRY: + raise UnresolvableDependencyException(dependency) + + for target in targets: + setattr(target, dependency, REGISTRY[dependency]) + finally: + _future_dependencies.clear() + + def reset(): """Reset the registry of providers. @@ -75,3 +105,4 @@ def reset(): """ REGISTRY.clear() + _future_dependencies.clear() diff --git a/keystone/common/logging.py b/keystone/common/logging.py index 8c036f02..d221edb7 100644 --- a/keystone/common/logging.py +++ b/keystone/common/logging.py @@ -23,7 +23,6 @@ import logging import logging.config import logging.handlers import pprint -import traceback # A list of things we want to replicate from logging. @@ -78,8 +77,7 @@ def fail_gracefully(f): try: return f(*args, **kw) except Exception as e: - # tracebacks are kept in the debug log - logging.debug(traceback.format_exc(e)) + logging.debug(e, exc_info=True) # exception message is printed to all logs logging.critical(e) diff --git a/keystone/common/wsgi.py b/keystone/common/wsgi.py index 381f1ff0..87636dbe 100644 --- a/keystone/common/wsgi.py +++ b/keystone/common/wsgi.py @@ -203,7 +203,7 @@ class BaseApplication(object): class Application(BaseApplication): - @webob.dec.wsgify + @webob.dec.wsgify(RequestClass=Request) def __call__(self, req): arg_dict = req.environ['wsgiorg.routing_args'][1] action = arg_dict.pop('action') diff --git a/keystone/identity/backends/ldap.py b/keystone/identity/backends/ldap.py index f9e546a9..91ea1e41 100644 --- a/keystone/identity/backends/ldap.py +++ b/keystone/identity/backends/ldap.py @@ -19,6 +19,7 @@ import uuid import ldap from keystone import clean +from keystone.common import dependency from keystone.common import ldap as common_ldap from keystone.common import logging from keystone.common import models @@ -38,6 +39,7 @@ DEFAULT_DOMAIN = { } +@dependency.requires('assignment_api') class Identity(identity.Driver): def __init__(self): super(Identity, self).__init__() @@ -77,7 +79,8 @@ class Identity(identity.Driver): return self.assignment_api._set_default_domain(ref) def list_users(self): - return self.assignment_api._set_default_domain(self.user.get_all()) + return (self.assignment_api._set_default_domain + (self.user.get_all_filtered())) def get_user_by_name(self, user_name, domain_id): self.assignment_api._validate_default_domain_id(domain_id) @@ -181,7 +184,7 @@ class Identity(identity.Driver): for user_dn in self.group.list_group_users(group_id): user_id = self.user._dn_to_id(user_dn) try: - users.append(self.user.get(user_id)) + users.append(self.user.get_filtered(user_id)) except exception.UserNotFound: LOG.debug(_("Group member '%(user_dn)s' not found in" " '%(group_id)s'. The user should be removed" @@ -264,6 +267,9 @@ class UserApi(common_ldap.EnabledEmuMixIn, common_ldap.BaseLdap): user = self.get(user_id) return identity.filter_user(user) + def get_all_filtered(self): + return [identity.filter_user(user) for user in self.get_all()] + class GroupApi(common_ldap.BaseLdap): DEFAULT_OU = 'ou=UserGroups' diff --git a/keystone/identity/backends/sql.py b/keystone/identity/backends/sql.py index ba97758c..bff41106 100644 --- a/keystone/identity/backends/sql.py +++ b/keystone/identity/backends/sql.py @@ -14,6 +14,7 @@ # License for the specific language governing permissions and limitations # under the License. +from keystone.common import dependency from keystone.common import sql from keystone.common.sql import migration from keystone.common import utils @@ -61,6 +62,7 @@ class UserGroupMembership(sql.ModelBase, sql.DictBase): primary_key=True) +@dependency.requires('assignment_api') class Identity(sql.Base, identity.Driver): def default_assignment_driver(self): return "keystone.assignment.backends.sql.Assignment" diff --git a/keystone/identity/controllers.py b/keystone/identity/controllers.py index 12fb8145..7ca1f8bf 100644 --- a/keystone/identity/controllers.py +++ b/keystone/identity/controllers.py @@ -403,6 +403,8 @@ class DomainV3(controller.V3Controller): @controller.protected def create_domain(self, context, domain): + self._require_attribute(domain, 'name') + ref = self._assign_unique_id(self._normalize_dict(domain)) ref = self.identity_api.create_domain(ref['id'], ref) return DomainV3.wrap_member(context, ref) @@ -544,6 +546,8 @@ class ProjectV3(controller.V3Controller): @controller.protected def create_project(self, context, project): + self._require_attribute(project, 'name') + ref = self._assign_unique_id(self._normalize_dict(project)) ref = self._normalize_domain_id(context, ref) ref = self.identity_api.create_project(ref['id'], ref) @@ -592,6 +596,8 @@ class UserV3(controller.V3Controller): @controller.protected def create_user(self, context, user): + self._require_attribute(user, 'name') + ref = self._assign_unique_id(self._normalize_dict(user)) ref = self._normalize_domain_id(context, ref) ref = self.identity_api.create_user(ref['id'], ref) @@ -663,6 +669,8 @@ class GroupV3(controller.V3Controller): @controller.protected def create_group(self, context, group): + self._require_attribute(group, 'name') + ref = self._assign_unique_id(self._normalize_dict(group)) ref = self._normalize_domain_id(context, ref) ref = self.identity_api.create_group(ref['id'], ref) @@ -713,6 +721,8 @@ class RoleV3(controller.V3Controller): @controller.protected def create_role(self, context, role): + self._require_attribute(role, 'name') + ref = self._assign_unique_id(self._normalize_dict(role)) ref = self.identity_api.create_role(ref['id'], ref) return RoleV3.wrap_member(context, ref) diff --git a/keystone/identity/core.py b/keystone/identity/core.py index 3d9bcf62..b2b3eaf0 100644 --- a/keystone/identity/core.py +++ b/keystone/identity/core.py @@ -16,7 +16,6 @@ """Main entry point into the Identity service.""" -from keystone import assignment from keystone import clean from keystone.common import dependency from keystone.common import logging @@ -53,6 +52,7 @@ def filter_user(user_ref): @dependency.provider('identity_api') +@dependency.requires('assignment_api') class Manager(manager.Manager): """Default pivot point for the Identity backend. @@ -61,12 +61,8 @@ class Manager(manager.Manager): """ - def __init__(self, assignment_api=None): + def __init__(self): super(Manager, self).__init__(CONF.identity.driver) - if assignment_api is None: - assignment_api = assignment.Manager(self) - self.assignment_api = assignment_api - self.driver.assignment_api = assignment_api def create_user(self, user_id, user_ref): user = user_ref.copy() diff --git a/keystone/service.py b/keystone/service.py index 6b0c3708..ce64aba8 100644 --- a/keystone/service.py +++ b/keystone/service.py @@ -16,8 +16,10 @@ import routes +from keystone import assignment from keystone import auth from keystone import catalog +from keystone.common import dependency from keystone.common import logging from keystone.common import wsgi from keystone import config @@ -34,16 +36,25 @@ from keystone import trust CONF = config.CONF LOG = logging.getLogger(__name__) + +# Ensure that the identity driver is created before the assignment manager. +# The default assignment driver is determined by the identity driver, so the +# identity driver must be available to the assignment manager. +_IDENTITY_API = identity.Manager() + DRIVERS = dict( + assignment_api=assignment.Manager(), catalog_api=catalog.Manager(), credentials_api=credential.Manager(), ec2_api=ec2.Manager(), - identity_api=identity.Manager(), + identity_api=_IDENTITY_API, policy_api=policy.Manager(), token_api=token.Manager(), trust_api=trust.Manager(), token_provider_api=token.provider.Manager()) +dependency.resolve_future_dependencies() + @logging.fail_gracefully def public_app_factory(global_conf, **local_conf): diff --git a/keystone/test.py b/keystone/test.py index cbe51fb2..9118b2ea 100644 --- a/keystone/test.py +++ b/keystone/test.py @@ -266,7 +266,11 @@ class TestCase(NoModule, unittest.TestCase): # only call load_backends once. dependency.reset() - for manager in [assignment, catalog, credential, ec2, identity, policy, + # NOTE(blk-u): identity must be before assignment to ensure that the + # identity driver is available to the assignment manager because the + # assignment manager gets the default assignment driver from the + # identity driver. + for manager in [identity, assignment, catalog, credential, ec2, policy, token, token_provider, trust]: # manager.__name__ is like keystone.xxx[.yyy], # converted to xxx[_yyy] @@ -276,6 +280,8 @@ class TestCase(NoModule, unittest.TestCase): setattr(self, manager_name, manager.Manager()) + dependency.resolve_future_dependencies() + def load_fixtures(self, fixtures): """Hacky basic and naive fixture loading based on a python module. diff --git a/keystone/trust/controllers.py b/keystone/trust/controllers.py index 7be6e8fd..7a94fe29 100644 --- a/keystone/trust/controllers.py +++ b/keystone/trust/controllers.py @@ -35,6 +35,21 @@ class TrustV3(controller.V3Controller): collection_name = "trusts" member_name = "trust" + @classmethod + def base_url(cls, path=None): + endpoint = CONF.public_endpoint % CONF + + # allow a missing trailing slash in the config + if endpoint[-1] != '/': + endpoint += '/' + + url = endpoint + 'v3/OS-TRUST' + + if path: + return url + path + else: + return url + '/' + cls.collection_name + def _get_user_id(self, context): if 'token_id' in context: token_id = context['token_id'] @@ -78,8 +93,7 @@ class TrustV3(controller.V3Controller): trust_full_roles.append(full_role) trust['roles'] = trust_full_roles trust['roles_links'] = { - 'self': (CONF.public_endpoint % CONF + - "trusts/%s/roles" % trust['id']), + 'self': (self.base_url() + "/%s/roles" % trust['id']), 'next': None, 'previous': None} diff --git a/tests/test_auth.py b/tests/test_auth.py index 1416269b..db5314be 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -618,6 +618,10 @@ class AuthWithTrust(AuthTest): role_ids = [self.role_browser['id'], self.role_member['id']] self.assertTrue(timeutils.parse_strtime(self.new_trust['expires_at'], fmt=TIME_FORMAT)) + self.assertIn('http://localhost:5000/v3/OS-TRUST/', + self.new_trust['links']['self']) + self.assertIn('http://localhost:5000/v3/OS-TRUST/', + self.new_trust['roles_links']['self']) for role in self.new_trust['roles']: self.assertIn(role['id'], role_ids) diff --git a/tests/test_backend_sql.py b/tests/test_backend_sql.py index 38eddaa4..89276e86 100644 --- a/tests/test_backend_sql.py +++ b/tests/test_backend_sql.py @@ -114,7 +114,7 @@ class SqlModels(SqlTests): def test_role_model(self): cols = (('id', sql.String, 64), - ('name', sql.String, 64)) + ('name', sql.String, 255)) self.assertExpectedSchema('role', cols) def test_user_project_metadata_model(self): diff --git a/tests/test_injection.py b/tests/test_injection.py index 08ccd7c7..36cd0126 100644 --- a/tests/test_injection.py +++ b/tests/test_injection.py @@ -21,6 +21,10 @@ from keystone.common import dependency class TestDependencyInjection(unittest.TestCase): + def tearDown(self): + dependency.reset() + super(TestDependencyInjection, self).tearDown() + def test_dependency_injection(self): class Interface(object): def do_work(self): @@ -165,6 +169,29 @@ class TestDependencyInjection(unittest.TestCase): with self.assertRaises(dependency.UnresolvableDependencyException): Consumer() + dependency.resolve_future_dependencies() + + def test_circular_dependency(self): + p1_name = uuid.uuid4().hex + p2_name = uuid.uuid4().hex + + @dependency.provider(p1_name) + @dependency.requires(p2_name) + class P1(object): + pass + + @dependency.provider(p2_name) + @dependency.requires(p1_name) + class P2(object): + pass + + p1 = P1() + p2 = P2() + + dependency.resolve_future_dependencies() + + self.assertIs(getattr(p1, p2_name), p2) + self.assertIs(getattr(p2, p1_name), p1) def test_reset(self): # Can reset the registry of providers. diff --git a/tests/test_v3_identity.py b/tests/test_v3_identity.py index 5eaf9085..891f0c6a 100644 --- a/tests/test_v3_identity.py +++ b/tests/test_v3_identity.py @@ -105,6 +105,10 @@ class IdentityTestCase(test_v3.RestfulTestCase): body={'domain': ref}) return self.assertValidDomainResponse(r, ref) + def test_create_domain_400(self): + """Call ``POST /domains``.""" + self.post('/domains', body={'domain': {}}, expected_status=400) + def test_list_domains(self): """Call ``GET /domains``.""" r = self.get('/domains') @@ -313,6 +317,10 @@ class IdentityTestCase(test_v3.RestfulTestCase): body={'project': ref}) self.assertValidProjectResponse(r, ref) + def test_create_project_400(self): + """Call ``POST /projects``.""" + self.post('/projects', body={'project': {}}, expected_status=400) + def test_get_project(self): """Call ``GET /projects/{project_id}``.""" r = self.get( @@ -376,6 +384,10 @@ class IdentityTestCase(test_v3.RestfulTestCase): body={'user': ref}) return self.assertValidUserResponse(r, ref) + def test_create_user_400(self): + """Call ``POST /users``.""" + self.post('/users', body={'user': {}}, expected_status=400) + def test_list_users(self): """Call ``GET /users``.""" r = self.get('/users') @@ -529,6 +541,10 @@ class IdentityTestCase(test_v3.RestfulTestCase): body={'group': ref}) return self.assertValidGroupResponse(r, ref) + def test_create_group_400(self): + """Call ``POST /groups``.""" + self.post('/groups', body={'group': {}}, expected_status=400) + def test_list_groups(self): """Call ``GET /groups``.""" r = self.get('/groups') @@ -569,6 +585,10 @@ class IdentityTestCase(test_v3.RestfulTestCase): body={'role': ref}) return self.assertValidRoleResponse(r, ref) + def test_create_role_400(self): + """Call ``POST /roles``.""" + self.post('/roles', body={'role': {}}, expected_status=400) + def test_list_roles(self): """Call ``GET /roles``.""" r = self.get('/roles') diff --git a/tests/test_wsgi.py b/tests/test_wsgi.py index 369dd952..003f7571 100644 --- a/tests/test_wsgi.py +++ b/tests/test_wsgi.py @@ -14,8 +14,6 @@ # License for the specific language governing permissions and limitations # under the License. -import webob - from keystone import test from keystone.common import wsgi @@ -34,7 +32,7 @@ class BaseWSGITest(test.TestCase): super(BaseWSGITest, self).setUp() def _make_request(self, url='/'): - req = webob.Request.blank(url) + req = wsgi.Request.blank(url) args = {'action': 'index', 'controller': None} req.environ['wsgiorg.routing_args'] = [None, args] return req |