From d95adc1ac82b34372cd037467d0f60200a6e0a72 Mon Sep 17 00:00:00 2001 From: Adam Young Date: Mon, 6 May 2013 14:09:07 -0400 Subject: [PATCH] extracting credentials Moves the credentials API into its own backend. LDAP was not going to be able to support credentials. Even with a custom schema, many people are using LDAP in read only mode, which means that they would not be able to use the credentials API at all. By splitting it out, we have a workable solution for both SQL and LDAP Identity backends. Drops the Foreign Key constraints off the Credentials table, as there is now no guaranttee that users are stored in the same backend. Blueprint extract-credentials-id Change-Id: I10ad4b36c6f03d1712621eaffcfefa48a5453aff --- etc/keystone.conf.sample | 3 + keystone/cli.py | 2 +- keystone/common/config.py | 4 + keystone/common/controller.py | 2 +- .../023_drop_credential_constraints.py | 78 +++++++++++++++ keystone/credential/__init__.py | 19 ++++ keystone/credential/backends/__init__.py | 0 keystone/credential/backends/sql.py | 96 +++++++++++++++++++ keystone/credential/controllers.py | 55 +++++++++++ keystone/credential/core.py | 87 +++++++++++++++++ keystone/credential/routers.py | 26 +++++ keystone/identity/backends/sql.py | 66 ------------- keystone/identity/controllers.py | 46 +-------- keystone/identity/core.py | 44 --------- keystone/identity/routers.py | 4 - keystone/service.py | 4 +- keystone/test.py | 2 + tests/test_v3_credential.py | 80 ++++++++++++++++ tests/test_v3_identity.py | 72 +++----------- 19 files changed, 473 insertions(+), 217 deletions(-) create mode 100644 keystone/common/sql/migrate_repo/versions/023_drop_credential_constraints.py create mode 100644 keystone/credential/__init__.py create mode 100644 keystone/credential/backends/__init__.py create mode 100644 keystone/credential/backends/sql.py create mode 100644 keystone/credential/controllers.py create mode 100644 keystone/credential/core.py create mode 100644 keystone/credential/routers.py create mode 100644 tests/test_v3_credential.py diff --git a/etc/keystone.conf.sample b/etc/keystone.conf.sample index 5344cdf30..21d3a07b0 100644 --- a/etc/keystone.conf.sample +++ b/etc/keystone.conf.sample @@ -91,6 +91,9 @@ # exist to order to maintain support for your v2 clients. # default_domain_id = default +[credential] +# driver = keystone.credential.backends.sql.Credential + [trust] # driver = keystone.trust.backends.sql.Trust diff --git a/keystone/cli.py b/keystone/cli.py index 5dace7fa0..b635878e9 100644 --- a/keystone/cli.py +++ b/keystone/cli.py @@ -48,7 +48,7 @@ class DbSync(BaseApp): @staticmethod def main(): - for k in ['identity', 'catalog', 'policy', 'token']: + for k in ['identity', 'catalog', 'policy', 'token', 'credential']: driver = importutils.import_object(getattr(CONF, k).driver) if hasattr(driver, 'db_sync'): driver.db_sync() diff --git a/keystone/common/config.py b/keystone/common/config.py index 877895109..da56a6821 100644 --- a/keystone/common/config.py +++ b/keystone/common/config.py @@ -268,6 +268,10 @@ def configure(): 'driver', group='identity', default='keystone.identity.backends.sql.Identity') + register_str( + 'driver', + group='credential', + default='keystone.credential.backends.sql.Credential') register_str( 'driver', group='policy', diff --git a/keystone/common/controller.py b/keystone/common/controller.py index 39fb8128c..ab6b5b533 100644 --- a/keystone/common/controller.py +++ b/keystone/common/controller.py @@ -149,7 +149,7 @@ def filterprotected(*filters): @dependency.requires('identity_api', 'policy_api', 'token_api', - 'trust_api', 'catalog_api') + 'trust_api', 'catalog_api', 'credential_api') class V2Controller(wsgi.Application): """Base controller class for Identity API v2.""" diff --git a/keystone/common/sql/migrate_repo/versions/023_drop_credential_constraints.py b/keystone/common/sql/migrate_repo/versions/023_drop_credential_constraints.py new file mode 100644 index 000000000..b6fbe54db --- /dev/null +++ b/keystone/common/sql/migrate_repo/versions/023_drop_credential_constraints.py @@ -0,0 +1,78 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack LLC +# +# 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. + + +import sqlalchemy +from sqlalchemy.orm import sessionmaker +from migrate import ForeignKeyConstraint + +MYSQL_FKEY_QUERY = ("select CONSTRAINT_NAME from " + "INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS " + "where table_name = 'credential'") + + +def drop_constraint_mysql(migrate_engine): + session = sessionmaker(bind=migrate_engine)() + #http://bugs.mysql.com/bug.php?id=10333 + #MySQL varies from the SQL norm in naming + #Foreign Keys. The mapping from the column name + #to the actual foreign key is stored in + #INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS + #SQLAlchemy expects the constraint name to be + # the column name. + for constraint in session.execute(MYSQL_FKEY_QUERY): + session.execute('ALTER TABLE credential DROP FOREIGN KEY %s;' + % constraint[0]) + session.commit() + + +def remove_constraints(migrate_engine): + if migrate_engine.name == 'sqlite': + return + if migrate_engine.name == 'mysql': + drop_constraint_mysql(migrate_engine) + return + meta = sqlalchemy.MetaData() + meta.bind = migrate_engine + user_table = sqlalchemy.Table('user', meta, autoload=True) + proj_table = sqlalchemy.Table('project', meta, autoload=True) + cred_table = sqlalchemy.Table('credential', meta, autoload=True) + ForeignKeyConstraint(columns=[cred_table.c.user_id], + refcolumns=[user_table.c.id]).drop() + ForeignKeyConstraint(columns=[cred_table.c.project_id], + refcolumns=[proj_table.c.id]).drop() + + +def add_constraints(migrate_engine): + if migrate_engine.name == 'sqlite': + return + meta = sqlalchemy.MetaData() + meta.bind = migrate_engine + user_table = sqlalchemy.Table('user', meta, autoload=True) + proj_table = sqlalchemy.Table('project', meta, autoload=True) + cred_table = sqlalchemy.Table('credential', meta, autoload=True) + ForeignKeyConstraint(columns=[cred_table.c.user_id], + refcolumns=[user_table.c.id]).create() + ForeignKeyConstraint(columns=[cred_table.c.project_id], + refcolumns=[proj_table.c.id]).create() + + +def upgrade(migrate_engine): + remove_constraints(migrate_engine) + + +def downgrade(migrate_engine): + add_constraints(migrate_engine) diff --git a/keystone/credential/__init__.py b/keystone/credential/__init__.py new file mode 100644 index 000000000..bef9c3c74 --- /dev/null +++ b/keystone/credential/__init__.py @@ -0,0 +1,19 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack LLC +# +# 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. + +from keystone.credential import controllers +from keystone.credential.core import * +from keystone.credential import routers diff --git a/keystone/credential/backends/__init__.py b/keystone/credential/backends/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/keystone/credential/backends/sql.py b/keystone/credential/backends/sql.py new file mode 100644 index 000000000..721cdc6d5 --- /dev/null +++ b/keystone/credential/backends/sql.py @@ -0,0 +1,96 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack LLC +# +# 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. + +from keystone import clean +from keystone.common import sql +from keystone.common.sql import migration +from keystone.common import utils +from keystone import credential +from keystone import exception + + +class CredentialModel(sql.ModelBase, sql.DictBase): + __tablename__ = 'credential' + attributes = ['id', 'user_id', 'project_id', 'blob', 'type'] + id = sql.Column(sql.String(64), primary_key=True) + user_id = sql.Column(sql.String(64), + nullable=False) + project_id = sql.Column(sql.String(64)) + blob = sql.Column(sql.JsonBlob(), nullable=False) + type = sql.Column(sql.String(255), nullable=False) + extra = sql.Column(sql.JsonBlob()) + + +class Credential(sql.Base, credential.Driver): + # Internal interface to manage the database + def db_sync(self): + migration.db_sync() + + # credential crud + + @sql.handle_conflicts(type='credential') + def create_credential(self, credential_id, credential): + session = self.get_session() + with session.begin(): + ref = CredentialModel.from_dict(credential) + session.add(ref) + session.flush() + return ref.to_dict() + + def list_credentials(self): + session = self.get_session() + refs = session.query(CredentialModel).all() + return [ref.to_dict() for ref in refs] + + def get_credential(self, credential_id): + session = self.get_session() + ref = (session.query(CredentialModel) + .filter_by(id=credential_id).first()) + if ref is None: + raise exception.CredentialNotFound(credential_id=credential_id) + return ref.to_dict() + + @sql.handle_conflicts(type='credential') + def update_credential(self, credential_id, credential): + session = self.get_session() + with session.begin(): + ref = (session.query(CredentialModel) + .filter_by(id=credential_id).first()) + if ref is None: + raise exception.CredentialNotFound(credential_id=credential_id) + old_dict = ref.to_dict() + for k in credential: + old_dict[k] = credential[k] + new_credential = CredentialModel.from_dict(old_dict) + for attr in CredentialModel.attributes: + if attr != 'id': + setattr(ref, attr, getattr(new_credential, attr)) + ref.extra = new_credential.extra + session.flush() + return ref.to_dict() + + def delete_credential(self, credential_id): + session = self.get_session() + + try: + ref = (session.query(CredentialModel) + .filter_by(id=credential_id).one()) + except sql.NotFound: + raise exception.CredentialNotFound(credential_id=credential_id) + + with session.begin(): + session.delete(ref) + session.flush() diff --git a/keystone/credential/controllers.py b/keystone/credential/controllers.py new file mode 100644 index 000000000..59289de38 --- /dev/null +++ b/keystone/credential/controllers.py @@ -0,0 +1,55 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack LLC +# +# 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. + +from keystone.common import controller + + +class CredentialV3(controller.V3Controller): + collection_name = 'credentials' + member_name = 'credential' + + @controller.protected + def create_credential(self, context, credential): + ref = self._assign_unique_id(self._normalize_dict(credential)) + ref = self.credential_api.create_credential(context, ref['id'], ref) + return CredentialV3.wrap_member(context, ref) + + @controller.protected + def list_credentials(self, context): + refs = self.credential_api.list_credentials(context) + return CredentialV3.wrap_collection(context, refs) + + @controller.protected + def get_credential(self, context, credential_id): + ref = self.credential_api.get_credential(context, credential_id) + return CredentialV3.wrap_member(context, ref) + + @controller.protected + def update_credential(self, context, credential_id, credential): + self._require_matching_id(credential_id, credential) + + ref = self.credential_api.update_credential( + context, + credential_id, + credential) + return CredentialV3.wrap_member(context, ref) + + def _delete_credential(self, context, credential_id): + return self.credential_api.delete_credential(context, credential_id) + + @controller.protected + def delete_credential(self, context, credential_id): + return self._delete_credential(context, credential_id) diff --git a/keystone/credential/core.py b/keystone/credential/core.py new file mode 100644 index 000000000..a8921ba02 --- /dev/null +++ b/keystone/credential/core.py @@ -0,0 +1,87 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack LLC +# +# 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. + +"""Main entry point into the Credentials service.""" + +from keystone.common import dependency +from keystone.common import logging +from keystone.common import manager +from keystone import config +from keystone import exception + + +CONF = config.CONF + +LOG = logging.getLogger(__name__) + + +@dependency.provider('credential_api') +class Manager(manager.Manager): + """Default pivot point for the Credential backend. + + See :mod:`keystone.common.manager.Manager` for more details on how this + dynamically calls the backend. + + """ + + def __init__(self): + super(Manager, self).__init__(CONF.credential.driver) + + +class Driver(object): + # credential crud + + def create_credential(self, credential_id, credential): + """Creates a new credential. + + :raises: keystone.exception.Conflict + + """ + raise exception.NotImplemented() + + def list_credentials(self): + """List all credentials in the system. + + :returns: a list of credential_refs or an empty list. + + """ + raise exception.NotImplemented() + + def get_credential(self, credential_id): + """Get a credential by ID. + + :returns: credential_ref + :raises: keystone.exception.CredentialNotFound + + """ + raise exception.NotImplemented() + + def update_credential(self, credential_id, credential): + """Updates an existing credential. + + :raises: keystone.exception.CredentialNotFound, + keystone.exception.Conflict + + """ + raise exception.NotImplemented() + + def delete_credential(self, credential_id): + """Deletes an existing credential. + + :raises: keystone.exception.CredentialNotFound + + """ + raise exception.NotImplemented() diff --git a/keystone/credential/routers.py b/keystone/credential/routers.py new file mode 100644 index 000000000..2b27a0393 --- /dev/null +++ b/keystone/credential/routers.py @@ -0,0 +1,26 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack LLC +# +# 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. + +"""WSGI Routers for the Credentials service.""" + +from keystone.common import router +from keystone.credential import controllers + + +def append_v3_routers(mapper, routers): + routers.append( + router.Router(controllers.CredentialV3(), + 'credentials', 'credential')) diff --git a/keystone/identity/backends/sql.py b/keystone/identity/backends/sql.py index 048a0b2b8..b1eb8ccc6 100644 --- a/keystone/identity/backends/sql.py +++ b/keystone/identity/backends/sql.py @@ -51,19 +51,6 @@ class Group(sql.ModelBase, sql.DictBase): __table_args__ = (sql.UniqueConstraint('domain_id', 'name'), {}) -class Credential(sql.ModelBase, sql.DictBase): - __tablename__ = 'credential' - attributes = ['id', 'user_id', 'project_id', 'blob', 'type'] - id = sql.Column(sql.String(64), primary_key=True) - user_id = sql.Column(sql.String(64), - sql.ForeignKey('user.id'), - nullable=False) - project_id = sql.Column(sql.String(64), sql.ForeignKey('project.id')) - blob = sql.Column(sql.JsonBlob(), nullable=False) - type = sql.Column(sql.String(255), nullable=False) - extra = sql.Column(sql.JsonBlob()) - - class Domain(sql.ModelBase, sql.DictBase): __tablename__ = 'domain' attributes = ['id', 'name', 'enabled'] @@ -857,59 +844,6 @@ class Identity(sql.Base, identity.Driver): session.delete(ref) session.flush() - # credential crud - - @sql.handle_conflicts(type='credential') - def create_credential(self, credential_id, credential): - session = self.get_session() - with session.begin(): - ref = Credential.from_dict(credential) - session.add(ref) - session.flush() - return ref.to_dict() - - def list_credentials(self): - session = self.get_session() - refs = session.query(Credential).all() - return [ref.to_dict() for ref in refs] - - def get_credential(self, credential_id): - session = self.get_session() - ref = session.query(Credential).filter_by(id=credential_id).first() - if ref is None: - raise exception.CredentialNotFound(credential_id=credential_id) - return ref.to_dict() - - @sql.handle_conflicts(type='credential') - def update_credential(self, credential_id, credential): - session = self.get_session() - with session.begin(): - ref = session.query(Credential).filter_by(id=credential_id).first() - if ref is None: - raise exception.CredentialNotFound(credential_id=credential_id) - old_dict = ref.to_dict() - for k in credential: - old_dict[k] = credential[k] - new_credential = Credential.from_dict(old_dict) - for attr in Credential.attributes: - if attr != 'id': - setattr(ref, attr, getattr(new_credential, attr)) - ref.extra = new_credential.extra - session.flush() - return ref.to_dict() - - def delete_credential(self, credential_id): - session = self.get_session() - - try: - ref = session.query(Credential).filter_by(id=credential_id).one() - except sql.NotFound: - raise exception.CredentialNotFound(credential_id=credential_id) - - with session.begin(): - session.delete(ref) - session.flush() - # role crud @sql.handle_conflicts(type='role') diff --git a/keystone/identity/controllers.py b/keystone/identity/controllers.py index 520db705b..34c6edc55 100644 --- a/keystone/identity/controllers.py +++ b/keystone/identity/controllers.py @@ -572,9 +572,9 @@ class ProjectV3(controller.V3Controller): def _delete_project(self, context, project_id): # Delete any credentials that reference this project - for cred in self.identity_api.list_credentials(context): + for cred in self.credential_api.list_credentials(context): if cred['project_id'] == project_id: - self.identity_api.delete_credential(context, cred['id']) + self.credential_api.delete_credential(context, cred['id']) # Finally delete the project itself - the backend is # responsible for deleting any role assignments related # to this project @@ -643,9 +643,9 @@ class UserV3(controller.V3Controller): def _delete_user(self, context, user_id): # Delete any credentials that reference this user - for cred in self.identity_api.list_credentials(context): + for cred in self.credential_api.list_credentials(context): if cred['user_id'] == user_id: - self.identity_api.delete_credential(context, cred['id']) + self.credential_api.delete_credential(context, cred['id']) # Make sure any tokens are marked as deleted self._delete_tokens_for_user(context, user_id) @@ -709,44 +709,6 @@ class GroupV3(controller.V3Controller): return self._delete_group(context, group_id) -class CredentialV3(controller.V3Controller): - collection_name = 'credentials' - member_name = 'credential' - - @controller.protected - def create_credential(self, context, credential): - ref = self._assign_unique_id(self._normalize_dict(credential)) - ref = self.identity_api.create_credential(context, ref['id'], ref) - return CredentialV3.wrap_member(context, ref) - - @controller.protected - def list_credentials(self, context): - refs = self.identity_api.list_credentials(context) - return CredentialV3.wrap_collection(context, refs) - - @controller.protected - def get_credential(self, context, credential_id): - ref = self.identity_api.get_credential(context, credential_id) - return CredentialV3.wrap_member(context, ref) - - @controller.protected - def update_credential(self, context, credential_id, credential): - self._require_matching_id(credential_id, credential) - - ref = self.identity_api.update_credential( - context, - credential_id, - credential) - return CredentialV3.wrap_member(context, ref) - - def _delete_credential(self, context, credential_id): - return self.identity_api.delete_credential(context, credential_id) - - @controller.protected - def delete_credential(self, context, credential_id): - return self._delete_credential(context, credential_id) - - class RoleV3(controller.V3Controller): collection_name = 'roles' member_name = 'role' diff --git a/keystone/identity/core.py b/keystone/identity/core.py index 2231b4a74..fde7ac8d0 100644 --- a/keystone/identity/core.py +++ b/keystone/identity/core.py @@ -432,50 +432,6 @@ class Driver(object): """ raise exception.NotImplemented() - # credential crud - - def create_credential(self, credential_id, credential): - """Creates a new credential. - - :raises: keystone.exception.Conflict - - """ - raise exception.NotImplemented() - - def list_credentials(self): - """List all credentials in the system. - - :returns: a list of credential_refs or an empty list. - - """ - raise exception.NotImplemented() - - def get_credential(self, credential_id): - """Get a credential by ID. - - :returns: credential_ref - :raises: keystone.exception.CredentialNotFound - - """ - raise exception.NotImplemented() - - def update_credential(self, credential_id, credential): - """Updates an existing credential. - - :raises: keystone.exception.CredentialNotFound, - keystone.exception.Conflict - - """ - raise exception.NotImplemented() - - def delete_credential(self, credential_id): - """Deletes an existing credential. - - :raises: keystone.exception.CredentialNotFound - - """ - raise exception.NotImplemented() - # role crud def create_role(self, role_id, role): diff --git a/keystone/identity/routers.py b/keystone/identity/routers.py index caa841491..32eada5e1 100644 --- a/keystone/identity/routers.py +++ b/keystone/identity/routers.py @@ -107,10 +107,6 @@ def append_v3_routers(mapper, routers): action='list_groups_for_user', conditions=dict(method=['GET'])) - routers.append( - router.Router(controllers.CredentialV3(), - 'credentials', 'credential')) - role_controller = controllers.RoleV3() routers.append(router.Router(role_controller, 'roles', 'role')) mapper.connect('/projects/{project_id}/users/{user_id}/roles/{role_id}', diff --git a/keystone/service.py b/keystone/service.py index 0b3338fb8..f41eb8c9c 100644 --- a/keystone/service.py +++ b/keystone/service.py @@ -23,6 +23,7 @@ from keystone import controllers from keystone.common import logging from keystone.common import wsgi from keystone.contrib import ec2 +from keystone import credential from keystone import identity from keystone import policy from keystone import routers @@ -35,6 +36,7 @@ LOG = logging.getLogger(__name__) DRIVERS = dict( catalog_api=catalog.Manager(), + credentials_api=credential.Manager(), ec2_api=ec2.Manager(), identity_api=identity.Manager(), policy_api=policy.Manager(), @@ -88,7 +90,7 @@ def v3_app_factory(global_conf, **local_conf): conf.update(local_conf) mapper = routes.Mapper() v3routers = [] - for module in [auth, catalog, identity, policy]: + for module in [auth, catalog, credential, identity, policy]: module.routers.append_v3_routers(mapper, v3routers) if CONF.trust.enabled: diff --git a/keystone/test.py b/keystone/test.py index 77d7f7cdd..f47c253ca 100644 --- a/keystone/test.py +++ b/keystone/test.py @@ -35,6 +35,7 @@ from keystone.common import logging from keystone.common import utils from keystone.common import wsgi from keystone import config +from keystone import credential from keystone import exception from keystone import identity from keystone.openstack.common import timeutils @@ -76,6 +77,7 @@ def testsdir(*p): def initialize_drivers(): DRIVERS['catalog_api'] = catalog.Manager() + DRIVERS['credential_api'] = credential.Manager() DRIVERS['identity_api'] = identity.Manager() DRIVERS['policy_api'] = policy.Manager() DRIVERS['token_api'] = token.Manager() diff --git a/tests/test_v3_credential.py b/tests/test_v3_credential.py new file mode 100644 index 000000000..eaad10d34 --- /dev/null +++ b/tests/test_v3_credential.py @@ -0,0 +1,80 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2013 OpenStack LLC +# +# 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. + +import uuid + +from keystone import exception + +import test_v3 + + +class CredentialTestCase(test_v3.RestfulTestCase): + """Test credential CRUD""" + def setUp(self): + + super(CredentialTestCase, self).setUp() + + self.credential_id = uuid.uuid4().hex + self.credential = self.new_credential_ref( + user_id=self.user['id'], + project_id=self.project_id) + self.credential['id'] = self.credential_id + self.credential_api.create_credential( + self.credential_id, + self.credential) + + def test_list_credentials(self): + """GET /credentials""" + r = self.get('/credentials') + self.assertValidCredentialListResponse(r, ref=self.credential) + + def test_list_credentials_xml(self): + """GET /credentials (xml data)""" + r = self.get('/credentials', content_type='xml') + self.assertValidCredentialListResponse(r, ref=self.credential) + + def test_create_credential(self): + """POST /credentials""" + ref = self.new_credential_ref(user_id=self.user['id']) + r = self.post( + '/credentials', + body={'credential': ref}) + self.assertValidCredentialResponse(r, ref) + + def test_get_credential(self): + """GET /credentials/{credential_id}""" + r = self.get( + '/credentials/%(credential_id)s' % { + 'credential_id': self.credential_id}) + self.assertValidCredentialResponse(r, self.credential) + + def test_update_credential(self): + """PATCH /credentials/{credential_id}""" + ref = self.new_credential_ref( + user_id=self.user['id'], + project_id=self.project_id) + del ref['id'] + r = self.patch( + '/credentials/%(credential_id)s' % { + 'credential_id': self.credential_id}, + body={'credential': ref}) + self.assertValidCredentialResponse(r, ref) + + def test_delete_credential(self): + """DELETE /credentials/{credential_id}""" + self.delete( + '/credentials/%(credential_id)s' % { + 'credential_id': self.credential_id}) diff --git a/tests/test_v3_identity.py b/tests/test_v3_identity.py index d212857b7..2e7ac242e 100644 --- a/tests/test_v3_identity.py +++ b/tests/test_v3_identity.py @@ -22,7 +22,7 @@ import test_v3 class IdentityTestCase(test_v3.RestfulTestCase): - """Test domains, projects, users, groups, credential & role CRUD""" + """Test domains, projects, users, groups, & role CRUD""" def setUp(self): super(IdentityTestCase, self).setUp() @@ -38,7 +38,7 @@ class IdentityTestCase(test_v3.RestfulTestCase): user_id=self.user['id'], project_id=self.project_id) self.credential['id'] = self.credential_id - self.identity_api.create_credential( + self.credential_api.create_credential( self.credential_id, self.credential) @@ -182,6 +182,7 @@ class IdentityTestCase(test_v3.RestfulTestCase): - Check entities in self.domain are unaffected """ + # Create a 2nd set of entities in a 2nd domain self.domain2 = self.new_domain_ref() self.identity_api.create_domain(self.domain2['id'], self.domain2) @@ -202,7 +203,7 @@ class IdentityTestCase(test_v3.RestfulTestCase): self.credential2 = self.new_credential_ref( user_id=self.user2['id'], project_id=self.project2['id']) - self.identity_api.create_credential( + self.credential_api.create_credential( self.credential2['id'], self.credential2) @@ -229,7 +230,7 @@ class IdentityTestCase(test_v3.RestfulTestCase): self.identity_api.get_user, user_id=self.user2['id']) self.assertRaises(exception.CredentialNotFound, - self.identity_api.get_credential, + self.credential_api.get_credential, credential_id=self.credential2['id']) # ...and that all self.domain entities are still here @@ -242,7 +243,7 @@ class IdentityTestCase(test_v3.RestfulTestCase): r = self.identity_api.get_user(self.user['id']) self.user.pop('password') self.assertDictEqual(r, self.user) - r = self.identity_api.get_credential(self.credential['id']) + r = self.credential_api.get_credential(self.credential['id']) self.assertDictEqual(r, self.credential) # project crud tests @@ -291,7 +292,7 @@ class IdentityTestCase(test_v3.RestfulTestCase): """ # First check the credential for this project is present - r = self.identity_api.get_credential(self.credential['id']) + r = self.credential_api.get_credential(self.credential['id']) self.assertDictEqual(r, self.credential) # Create a second credential with a different project self.project2 = self.new_project_ref( @@ -300,7 +301,7 @@ class IdentityTestCase(test_v3.RestfulTestCase): self.credential2 = self.new_credential_ref( user_id=self.user['id'], project_id=self.project2['id']) - self.identity_api.create_credential( + self.credential_api.create_credential( self.credential2['id'], self.credential2) @@ -312,10 +313,10 @@ class IdentityTestCase(test_v3.RestfulTestCase): # Deleting the project should have deleted any credentials # that reference this project self.assertRaises(exception.CredentialNotFound, - self.identity_api.get_credential, + self.credential_api.get_credential, credential_id=self.credential['id']) # But the credential for project2 is unaffected - r = self.identity_api.get_credential(self.credential2['id']) + r = self.credential_api.get_credential(self.credential2['id']) self.assertDictEqual(r, self.credential2) # user crud tests @@ -429,7 +430,7 @@ class IdentityTestCase(test_v3.RestfulTestCase): """ # First check the credential for this user is present - r = self.identity_api.get_credential(self.credential['id']) + r = self.credential_api.get_credential(self.credential['id']) self.assertDictEqual(r, self.credential) # Create a second credential with a different user self.user2 = self.new_user_ref( @@ -439,7 +440,7 @@ class IdentityTestCase(test_v3.RestfulTestCase): self.credential2 = self.new_credential_ref( user_id=self.user2['id'], project_id=self.project['id']) - self.identity_api.create_credential( + self.credential_api.create_credential( self.credential2['id'], self.credential2) # Create a token for this user which we can check later @@ -462,13 +463,13 @@ class IdentityTestCase(test_v3.RestfulTestCase): # Deleting the user should have deleted any credentials # that reference this project self.assertRaises(exception.CredentialNotFound, - self.identity_api.get_credential, + self.credential_api.get_credential, credential_id=self.credential['id']) # And the no tokens we remain valid tokens = self.token_api.list_tokens(self.user['id']) self.assertEquals(len(tokens), 0) # But the credential for user2 is unaffected - r = self.identity_api.get_credential(self.credential2['id']) + r = self.credential_api.get_credential(self.credential2['id']) self.assertDictEqual(r, self.credential2) # group crud tests @@ -511,51 +512,6 @@ class IdentityTestCase(test_v3.RestfulTestCase): self.delete('/groups/%(group_id)s' % { 'group_id': self.group_id}) - # credential crud tests - - def test_list_credentials(self): - """GET /credentials""" - r = self.get('/credentials') - self.assertValidCredentialListResponse(r, ref=self.credential) - - def test_list_credentials_xml(self): - """GET /credentials (xml data)""" - r = self.get('/credentials', content_type='xml') - self.assertValidCredentialListResponse(r, ref=self.credential) - - def test_create_credential(self): - """POST /credentials""" - ref = self.new_credential_ref(user_id=self.user['id']) - r = self.post( - '/credentials', - body={'credential': ref}) - self.assertValidCredentialResponse(r, ref) - - def test_get_credential(self): - """GET /credentials/{credential_id}""" - r = self.get( - '/credentials/%(credential_id)s' % { - 'credential_id': self.credential_id}) - self.assertValidCredentialResponse(r, self.credential) - - def test_update_credential(self): - """PATCH /credentials/{credential_id}""" - ref = self.new_credential_ref( - user_id=self.user['id'], - project_id=self.project_id) - del ref['id'] - r = self.patch( - '/credentials/%(credential_id)s' % { - 'credential_id': self.credential_id}, - body={'credential': ref}) - self.assertValidCredentialResponse(r, ref) - - def test_delete_credential(self): - """DELETE /credentials/{credential_id}""" - self.delete( - '/credentials/%(credential_id)s' % { - 'credential_id': self.credential_id}) - # role crud tests def test_create_role(self):