From 2e7b4f2027a1147ca28301e4f88adf8274b39a1f Mon Sep 17 00:00:00 2001 From: DUVAL Thomas Date: Thu, 9 Jun 2016 09:11:50 +0200 Subject: Update Keystone core to Mitaka. Change-Id: Ia10d6add16f4a9d25d1f42d420661c46332e69db --- keystone-moon/keystone/tests/unit/__init__.py | 19 + .../keystone/tests/unit/assignment/__init__.py | 0 .../unit/assignment/role_backends/__init__.py | 0 .../unit/assignment/role_backends/test_sql.py | 112 + .../tests/unit/assignment/test_backends.py | 3755 +++++++++++++++ .../keystone/tests/unit/assignment/test_core.py | 123 + .../keystone/tests/unit/backend/core_ldap.py | 4 +- .../tests/unit/backend/legacy_drivers/__init__.py | 0 .../legacy_drivers/assignment/V8/__init__.py | 0 .../backend/legacy_drivers/assignment/V8/sql.py | 39 + .../backend/legacy_drivers/assignment/__init__.py | 0 .../legacy_drivers/federation/V8/__init__.py | 0 .../backend/legacy_drivers/federation/V8/api_v3.py | 108 + .../backend/legacy_drivers/federation/__init__.py | 0 .../backend/legacy_drivers/resource/V8/__init__.py | 0 .../unit/backend/legacy_drivers/resource/V8/sql.py | 71 + .../backend/legacy_drivers/resource/__init__.py | 0 .../backend/legacy_drivers/role/V8/__init__.py | 0 .../unit/backend/legacy_drivers/role/V8/sql.py | 30 + .../unit/backend/legacy_drivers/role/__init__.py | 0 .../keystone/tests/unit/catalog/test_backends.py | 588 +++ .../keystone/tests/unit/catalog/test_core.py | 30 +- .../tests/unit/common/test_authorization.py | 161 + .../keystone/tests/unit/common/test_ldap.py | 36 +- .../keystone/tests/unit/common/test_manager.py | 5 +- .../tests/unit/common/test_notifications.py | 329 +- .../keystone/tests/unit/common/test_sql_core.py | 10 +- .../keystone/tests/unit/common/test_utils.py | 48 +- .../tests/unit/config_files/backend_ldap_sql.conf | 2 +- .../tests/unit/config_files/backend_liveldap.conf | 4 - .../tests/unit/config_files/backend_mysql.conf | 2 +- .../unit/config_files/backend_pool_liveldap.conf | 3 - .../tests/unit/config_files/backend_sql.conf | 2 +- .../unit/config_files/backend_tls_liveldap.conf | 3 - .../keystone.Default.conf | 2 +- .../keystone.domain1.conf | 3 +- .../tests/unit/contrib/federation/test_utils.py | 299 +- keystone-moon/keystone/tests/unit/core.py | 388 +- .../keystone/tests/unit/default_fixtures.py | 61 +- .../keystone/tests/unit/external/README.rst | 9 + .../keystone/tests/unit/external/__init__.py | 0 .../keystone/tests/unit/external/test_timeutils.py | 33 + keystone-moon/keystone/tests/unit/fakeldap.py | 61 +- keystone-moon/keystone/tests/unit/filtering.py | 3 - .../keystone/tests/unit/identity/test_backends.py | 1297 ++++++ .../tests/unit/identity/test_controllers.py | 65 + .../keystone/tests/unit/identity/test_core.py | 4 +- .../keystone/tests/unit/identity_mapping.py | 7 +- .../keystone/tests/unit/ksfixtures/__init__.py | 2 + .../keystone/tests/unit/ksfixtures/appserver.py | 6 +- .../keystone/tests/unit/ksfixtures/auth_plugins.py | 34 + .../keystone/tests/unit/ksfixtures/cache.py | 17 +- .../keystone/tests/unit/ksfixtures/database.py | 75 +- .../keystone/tests/unit/ksfixtures/hacking.py | 176 +- .../keystone/tests/unit/ksfixtures/ldapdb.py | 3 +- .../keystone/tests/unit/ksfixtures/policy.py | 33 + .../keystone/tests/unit/mapping_fixtures.py | 176 +- .../keystone/tests/unit/policy/__init__.py | 0 .../keystone/tests/unit/policy/test_backends.py | 86 + .../keystone/tests/unit/resource/__init__.py | 0 .../tests/unit/resource/backends/__init__.py | 0 .../tests/unit/resource/backends/test_sql.py | 24 + .../unit/resource/config_backends/__init__.py | 0 .../unit/resource/config_backends/test_sql.py | 53 + .../keystone/tests/unit/resource/test_backends.py | 1669 +++++++ .../tests/unit/resource/test_controllers.py | 57 + .../keystone/tests/unit/resource/test_core.py | 692 +++ keystone-moon/keystone/tests/unit/rest.py | 28 +- .../keystone/tests/unit/schema/__init__.py | 0 keystone-moon/keystone/tests/unit/schema/v2.py | 161 + .../test_associate_project_endpoint_extension.py | 453 +- keystone-moon/keystone/tests/unit/test_auth.py | 202 +- .../keystone/tests/unit/test_auth_plugin.py | 2 +- .../tests/unit/test_backend_endpoint_policy.py | 23 +- .../tests/unit/test_backend_id_mapping_sql.py | 5 +- .../keystone/tests/unit/test_backend_kvs.py | 66 +- .../keystone/tests/unit/test_backend_ldap.py | 1285 +++--- .../keystone/tests/unit/test_backend_ldap_pool.py | 29 +- .../keystone/tests/unit/test_backend_rules.py | 19 +- .../keystone/tests/unit/test_backend_sql.py | 619 +-- .../keystone/tests/unit/test_backend_templated.py | 52 +- keystone-moon/keystone/tests/unit/test_catalog.py | 131 +- .../keystone/tests/unit/test_cert_setup.py | 37 +- keystone-moon/keystone/tests/unit/test_cli.py | 242 +- keystone-moon/keystone/tests/unit/test_config.py | 2 +- .../keystone/tests/unit/test_contrib_s3_core.py | 56 +- .../tests/unit/test_contrib_simple_cert.py | 10 +- .../keystone/tests/unit/test_credential.py | 265 ++ .../keystone/tests/unit/test_driver_hints.py | 2 +- .../keystone/tests/unit/test_entry_points.py | 48 + .../keystone/tests/unit/test_exception.py | 74 +- .../keystone/tests/unit/test_hacking_checks.py | 42 +- keystone-moon/keystone/tests/unit/test_kvs.py | 38 +- .../keystone/tests/unit/test_ldap_livetest.py | 10 +- .../keystone/tests/unit/test_ldap_pool_livetest.py | 3 +- .../keystone/tests/unit/test_ldap_tls_livetest.py | 4 + .../keystone/tests/unit/test_middleware.py | 620 +-- keystone-moon/keystone/tests/unit/test_policy.py | 41 +- keystone-moon/keystone/tests/unit/test_revoke.py | 76 +- .../keystone/tests/unit/test_sql_livetest.py | 24 - .../tests/unit/test_sql_migrate_extensions.py | 353 +- .../keystone/tests/unit/test_sql_upgrade.py | 1234 +++-- .../keystone/tests/unit/test_token_provider.py | 10 +- .../keystone/tests/unit/test_url_middleware.py | 1 + keystone-moon/keystone/tests/unit/test_v2.py | 150 +- .../keystone/tests/unit/test_v2_controller.py | 75 +- keystone-moon/keystone/tests/unit/test_v3.py | 681 ++- .../keystone/tests/unit/test_v3_assignment.py | 2419 +++++----- keystone-moon/keystone/tests/unit/test_v3_auth.py | 4769 +++++++++++--------- .../keystone/tests/unit/test_v3_catalog.py | 349 +- .../keystone/tests/unit/test_v3_credential.py | 242 +- .../keystone/tests/unit/test_v3_domain_config.py | 259 +- .../keystone/tests/unit/test_v3_endpoint_policy.py | 58 +- .../keystone/tests/unit/test_v3_federation.py | 562 ++- .../keystone/tests/unit/test_v3_filters.py | 57 +- .../keystone/tests/unit/test_v3_identity.py | 461 +- .../keystone/tests/unit/test_v3_oauth1.py | 66 +- .../keystone/tests/unit/test_v3_os_revoke.py | 10 +- .../keystone/tests/unit/test_v3_policy.py | 29 +- .../keystone/tests/unit/test_v3_protection.py | 739 ++- .../keystone/tests/unit/test_v3_resource.py | 1434 ++++++ keystone-moon/keystone/tests/unit/test_v3_trust.py | 403 ++ .../keystone/tests/unit/test_validation.py | 352 +- keystone-moon/keystone/tests/unit/test_versions.py | 257 +- keystone-moon/keystone/tests/unit/test_wsgi.py | 141 +- .../keystone/tests/unit/tests/test_core.py | 2 +- .../keystone/tests/unit/token/test_backends.py | 551 +++ .../tests/unit/token/test_fernet_provider.py | 428 +- .../keystone/tests/unit/token/test_provider.py | 4 +- .../tests/unit/token/test_token_data_helper.py | 3 +- .../keystone/tests/unit/token/test_token_model.py | 2 +- .../keystone/tests/unit/trust/__init__.py | 0 .../keystone/tests/unit/trust/test_backends.py | 172 + keystone-moon/keystone/tests/unit/utils.py | 4 - 134 files changed, 23383 insertions(+), 8382 deletions(-) create mode 100644 keystone-moon/keystone/tests/unit/assignment/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/assignment/role_backends/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/assignment/role_backends/test_sql.py create mode 100644 keystone-moon/keystone/tests/unit/assignment/test_backends.py create mode 100644 keystone-moon/keystone/tests/unit/assignment/test_core.py create mode 100644 keystone-moon/keystone/tests/unit/backend/legacy_drivers/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/backend/legacy_drivers/assignment/V8/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/backend/legacy_drivers/assignment/V8/sql.py create mode 100644 keystone-moon/keystone/tests/unit/backend/legacy_drivers/assignment/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/backend/legacy_drivers/federation/V8/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/backend/legacy_drivers/federation/V8/api_v3.py create mode 100644 keystone-moon/keystone/tests/unit/backend/legacy_drivers/federation/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/backend/legacy_drivers/resource/V8/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/backend/legacy_drivers/resource/V8/sql.py create mode 100644 keystone-moon/keystone/tests/unit/backend/legacy_drivers/resource/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/backend/legacy_drivers/role/V8/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/backend/legacy_drivers/role/V8/sql.py create mode 100644 keystone-moon/keystone/tests/unit/backend/legacy_drivers/role/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/catalog/test_backends.py create mode 100644 keystone-moon/keystone/tests/unit/common/test_authorization.py create mode 100644 keystone-moon/keystone/tests/unit/external/README.rst create mode 100644 keystone-moon/keystone/tests/unit/external/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/external/test_timeutils.py create mode 100644 keystone-moon/keystone/tests/unit/identity/test_backends.py create mode 100644 keystone-moon/keystone/tests/unit/identity/test_controllers.py create mode 100644 keystone-moon/keystone/tests/unit/ksfixtures/auth_plugins.py create mode 100644 keystone-moon/keystone/tests/unit/ksfixtures/policy.py create mode 100644 keystone-moon/keystone/tests/unit/policy/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/policy/test_backends.py create mode 100644 keystone-moon/keystone/tests/unit/resource/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/resource/backends/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/resource/backends/test_sql.py create mode 100644 keystone-moon/keystone/tests/unit/resource/config_backends/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/resource/config_backends/test_sql.py create mode 100644 keystone-moon/keystone/tests/unit/resource/test_backends.py create mode 100644 keystone-moon/keystone/tests/unit/resource/test_controllers.py create mode 100644 keystone-moon/keystone/tests/unit/resource/test_core.py create mode 100644 keystone-moon/keystone/tests/unit/schema/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/schema/v2.py create mode 100644 keystone-moon/keystone/tests/unit/test_credential.py create mode 100644 keystone-moon/keystone/tests/unit/test_entry_points.py create mode 100644 keystone-moon/keystone/tests/unit/test_v3_resource.py create mode 100644 keystone-moon/keystone/tests/unit/test_v3_trust.py create mode 100644 keystone-moon/keystone/tests/unit/token/test_backends.py create mode 100644 keystone-moon/keystone/tests/unit/trust/__init__.py create mode 100644 keystone-moon/keystone/tests/unit/trust/test_backends.py (limited to 'keystone-moon/keystone/tests/unit') diff --git a/keystone-moon/keystone/tests/unit/__init__.py b/keystone-moon/keystone/tests/unit/__init__.py index 52af8dfc..0e92ca65 100644 --- a/keystone-moon/keystone/tests/unit/__init__.py +++ b/keystone-moon/keystone/tests/unit/__init__.py @@ -13,6 +13,25 @@ # under the License. import oslo_i18n +import six + + +if six.PY3: + # NOTE(dstanek): This block will monkey patch libraries that are not + # yet supported in Python3. We do this that that it is possible to + # execute any tests at all. Without monkey patching modules the + # tests will fail with import errors. + + import sys + from unittest import mock # noqa: our import detection is naive? + + sys.modules['ldap'] = mock.Mock() + sys.modules['ldap.controls'] = mock.Mock() + sys.modules['ldap.dn'] = mock.Mock() + sys.modules['ldap.filter'] = mock.Mock() + sys.modules['ldap.modlist'] = mock.Mock() + sys.modules['ldappool'] = mock.Mock() + # NOTE(dstanek): oslo_i18n.enable_lazy() must be called before # keystone.i18n._() is called to ensure it has the desired lazy lookup diff --git a/keystone-moon/keystone/tests/unit/assignment/__init__.py b/keystone-moon/keystone/tests/unit/assignment/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/unit/assignment/role_backends/__init__.py b/keystone-moon/keystone/tests/unit/assignment/role_backends/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/unit/assignment/role_backends/test_sql.py b/keystone-moon/keystone/tests/unit/assignment/role_backends/test_sql.py new file mode 100644 index 00000000..37e2d924 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/assignment/role_backends/test_sql.py @@ -0,0 +1,112 @@ +# 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.common import sql +from keystone import exception +from keystone.tests import unit +from keystone.tests.unit.assignment import test_core +from keystone.tests.unit.backend import core_sql + + +class SqlRoleModels(core_sql.BaseBackendSqlModels): + + def test_role_model(self): + cols = (('id', sql.String, 64), + ('name', sql.String, 255), + ('domain_id', sql.String, 64)) + self.assertExpectedSchema('role', cols) + + +class SqlRole(core_sql.BaseBackendSqlTests, test_core.RoleTests): + + def test_create_null_role_name(self): + role = unit.new_role_ref(name=None) + self.assertRaises(exception.UnexpectedError, + self.role_api.create_role, + role['id'], + role) + self.assertRaises(exception.RoleNotFound, + self.role_api.get_role, + role['id']) + + def test_create_duplicate_role_domain_specific_name_fails(self): + domain = unit.new_domain_ref() + role1 = unit.new_role_ref(domain_id=domain['id']) + self.role_api.create_role(role1['id'], role1) + role2 = unit.new_role_ref(name=role1['name'], + domain_id=domain['id']) + self.assertRaises(exception.Conflict, + self.role_api.create_role, + role2['id'], + role2) + + def test_update_domain_id_of_role_fails(self): + # Create a global role + role1 = unit.new_role_ref() + role1 = self.role_api.create_role(role1['id'], role1) + # Try and update it to be domain specific + domainA = unit.new_domain_ref() + role1['domain_id'] = domainA['id'] + self.assertRaises(exception.ValidationError, + self.role_api.update_role, + role1['id'], + role1) + + # Create a domain specific role from scratch + role2 = unit.new_role_ref(domain_id=domainA['id']) + self.role_api.create_role(role2['id'], role2) + # Try to "move" it to another domain + domainB = unit.new_domain_ref() + role2['domain_id'] = domainB['id'] + self.assertRaises(exception.ValidationError, + self.role_api.update_role, + role2['id'], + role2) + # Now try to make it global + role2['domain_id'] = None + self.assertRaises(exception.ValidationError, + self.role_api.update_role, + role2['id'], + role2) + + def test_domain_specific_separation(self): + domain1 = unit.new_domain_ref() + role1 = unit.new_role_ref(domain_id=domain1['id']) + role_ref1 = self.role_api.create_role(role1['id'], role1) + self.assertDictEqual(role1, role_ref1) + # Check we can have the same named role in a different domain + domain2 = unit.new_domain_ref() + role2 = unit.new_role_ref(name=role1['name'], domain_id=domain2['id']) + role_ref2 = self.role_api.create_role(role2['id'], role2) + self.assertDictEqual(role2, role_ref2) + # ...and in fact that you can have the same named role as a global role + role3 = unit.new_role_ref(name=role1['name']) + role_ref3 = self.role_api.create_role(role3['id'], role3) + self.assertDictEqual(role3, role_ref3) + # Check that updating one doesn't change the others + role1['name'] = uuid.uuid4().hex + self.role_api.update_role(role1['id'], role1) + role_ref1 = self.role_api.get_role(role1['id']) + self.assertDictEqual(role1, role_ref1) + role_ref2 = self.role_api.get_role(role2['id']) + self.assertDictEqual(role2, role_ref2) + role_ref3 = self.role_api.get_role(role3['id']) + self.assertDictEqual(role3, role_ref3) + # Check that deleting one of these, doesn't affect the others + self.role_api.delete_role(role1['id']) + self.assertRaises(exception.RoleNotFound, + self.role_api.get_role, + role1['id']) + self.role_api.get_role(role2['id']) + self.role_api.get_role(role3['id']) diff --git a/keystone-moon/keystone/tests/unit/assignment/test_backends.py b/keystone-moon/keystone/tests/unit/assignment/test_backends.py new file mode 100644 index 00000000..eb40e569 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/assignment/test_backends.py @@ -0,0 +1,3755 @@ +# 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 + +import mock +from oslo_config import cfg +from six.moves import range +from testtools import matchers + +from keystone import exception +from keystone.tests import unit + + +CONF = cfg.CONF + + +class AssignmentTestHelperMixin(object): + """Mixin class to aid testing of assignments. + + This class supports data driven test plans that enable: + + - Creation of initial entities, such as domains, users, groups, projects + and roles + - Creation of assignments referencing the above entities + - A set of input parameters and expected outputs to list_role_assignments + based on the above test data + + A test plan is a dict of the form: + + test_plan = { + entities: details and number of entities, + group_memberships: group-user entity memberships, + assignments: list of assignments to create, + tests: list of pairs of input params and expected outputs} + + An example test plan: + + test_plan = { + # First, create the entities required. Entities are specified by + # a dict with the key being the entity type and the value an + # entity specification which can be one of: + # + # - a simple number, e.g. {'users': 3} creates 3 users + # - a dict where more information regarding the contents of the entity + # is required, e.g. {'domains' : {'users : 3}} creates a domain + # with three users + # - a list of entity specifications if multiple are required + # + # The following creates a domain that contains a single user, group and + # project, as well as creating three roles. + + 'entities': {'domains': {'users': 1, 'groups': 1, 'projects': 1}, + 'roles': 3}, + + # If it is required that an existing domain be used for the new + # entities, then the id of that domain can be included in the + # domain dict. For example, if alternatively we wanted to add 3 users + # to the default domain, add a second domain containing 3 projects as + # well as 5 additional empty domains, the entities would be defined as: + # + # 'entities': {'domains': [{'id': DEFAULT_DOMAIN, 'users': 3}, + # {'projects': 3}, 5]}, + # + # A project hierarchy can be specified within the 'projects' section by + # nesting the 'project' key, for example to create a project with three + # sub-projects you would use: + + 'projects': {'project': 3} + + # A more complex hierarchy can also be defined, for example the + # following would define three projects each containing a + # sub-project, each of which contain a further three sub-projects. + + 'projects': [{'project': {'project': 3}}, + {'project': {'project': 3}}, + {'project': {'project': 3}}] + + # If the 'roles' entity count is defined as top level key in 'entities' + # dict then these are global roles. If it is placed within the + # 'domain' dict, then they will be domain specific roles. A mix of + # domain specific and global roles are allowed, with the role index + # being calculated in the order they are defined in the 'entities' + # dict. + + # A set of implied role specifications. In this case, prior role + # index 0 implies role index 1, and role 1 implies roles 2 and 3. + + 'roles': [{'role': 0, 'implied_roles': [1]}, + {'role': 1, 'implied_roles': [2, 3]}] + + # A list of groups and their members. In this case make users with + # index 0 and 1 members of group with index 0. Users and Groups are + # indexed in the order they appear in the 'entities' key above. + + 'group_memberships': [{'group': 0, 'users': [0, 1]}] + + # Next, create assignments between the entities, referencing the + # entities by index, i.e. 'user': 0 refers to user[0]. Entities are + # indexed in the order they appear in the 'entities' key above within + # their entity type. + + 'assignments': [{'user': 0, 'role': 0, 'domain': 0}, + {'user': 0, 'role': 1, 'project': 0}, + {'group': 0, 'role': 2, 'domain': 0}, + {'user': 0, 'role': 2, 'project': 0}], + + # Finally, define an array of tests where list_role_assignment() is + # called with the given input parameters and the results are then + # confirmed to be as given in 'results'. Again, all entities are + # referenced by index. + + 'tests': [ + {'params': {}, + 'results': [{'user': 0, 'role': 0, 'domain': 0}, + {'user': 0, 'role': 1, 'project': 0}, + {'group': 0, 'role': 2, 'domain': 0}, + {'user': 0, 'role': 2, 'project': 0}]}, + {'params': {'role': 2}, + 'results': [{'group': 0, 'role': 2, 'domain': 0}, + {'user': 0, 'role': 2, 'project': 0}]}] + + # The 'params' key also supports the 'effective', + # 'inherited_to_projects' and 'source_from_group_ids' options to + # list_role_assignments.} + + """ + + def _handle_project_spec(self, test_data, domain_id, project_spec, + parent_id=None): + """Handle the creation of a project or hierarchy of projects. + + project_spec may either be a count of the number of projects to + create, or it may be a list of the form: + + [{'project': project_spec}, {'project': project_spec}, ...] + + This method is called recursively to handle the creation of a + hierarchy of projects. + + """ + def _create_project(domain_id, parent_id): + new_project = unit.new_project_ref(domain_id=domain_id, + parent_id=parent_id) + new_project = self.resource_api.create_project(new_project['id'], + new_project) + return new_project + + if isinstance(project_spec, list): + for this_spec in project_spec: + self._handle_project_spec( + test_data, domain_id, this_spec, parent_id=parent_id) + elif isinstance(project_spec, dict): + new_proj = _create_project(domain_id, parent_id) + test_data['projects'].append(new_proj) + self._handle_project_spec( + test_data, domain_id, project_spec['project'], + parent_id=new_proj['id']) + else: + for _ in range(project_spec): + test_data['projects'].append( + _create_project(domain_id, parent_id)) + + def _create_role(self, domain_id=None): + new_role = unit.new_role_ref(domain_id=domain_id) + return self.role_api.create_role(new_role['id'], new_role) + + def _handle_domain_spec(self, test_data, domain_spec): + """Handle the creation of domains and their contents. + + domain_spec may either be a count of the number of empty domains to + create, a dict describing the domain contents, or a list of + domain_specs. + + In the case when a list is provided, this method calls itself + recursively to handle the list elements. + + This method will insert any entities created into test_data + + """ + def _create_domain(domain_id=None): + if domain_id is None: + new_domain = unit.new_domain_ref() + self.resource_api.create_domain(new_domain['id'], + new_domain) + return new_domain + else: + # The test plan specified an existing domain to use + return self.resource_api.get_domain(domain_id) + + def _create_entity_in_domain(entity_type, domain_id): + """Create a user or group entity in the domain.""" + if entity_type == 'users': + new_entity = unit.new_user_ref(domain_id=domain_id) + new_entity = self.identity_api.create_user(new_entity) + elif entity_type == 'groups': + new_entity = unit.new_group_ref(domain_id=domain_id) + new_entity = self.identity_api.create_group(new_entity) + elif entity_type == 'roles': + new_entity = self._create_role(domain_id=domain_id) + else: + # Must be a bad test plan + raise exception.NotImplemented() + return new_entity + + if isinstance(domain_spec, list): + for x in domain_spec: + self._handle_domain_spec(test_data, x) + elif isinstance(domain_spec, dict): + # If there is a domain ID specified, then use it + the_domain = _create_domain(domain_spec.get('id')) + test_data['domains'].append(the_domain) + for entity_type, value in domain_spec.items(): + if entity_type == 'id': + # We already used this above to determine whether to + # use and existing domain + continue + if entity_type == 'projects': + # If it's projects, we need to handle the potential + # specification of a project hierarchy + self._handle_project_spec( + test_data, the_domain['id'], value) + else: + # It's a count of number of entities + for _ in range(value): + test_data[entity_type].append( + _create_entity_in_domain( + entity_type, the_domain['id'])) + else: + for _ in range(domain_spec): + test_data['domains'].append(_create_domain()) + + def create_entities(self, entity_pattern): + """Create the entities specified in the test plan. + + Process the 'entities' key in the test plan, creating the requested + entities. Each created entity will be added to the array of entities + stored in the returned test_data object, e.g.: + + test_data['users'] = [user[0], user[1]....] + + """ + test_data = {} + for entity in ['users', 'groups', 'domains', 'projects', 'roles']: + test_data[entity] = [] + + # Create any domains requested and, if specified, any entities within + # those domains + if 'domains' in entity_pattern: + self._handle_domain_spec(test_data, entity_pattern['domains']) + + # Create any roles requested + if 'roles' in entity_pattern: + for _ in range(entity_pattern['roles']): + test_data['roles'].append(self._create_role()) + + return test_data + + def _convert_entity_shorthand(self, key, shorthand_data, reference_data): + """Convert a shorthand entity description into a full ID reference. + + In test plan definitions, we allow a shorthand for referencing to an + entity of the form: + + 'user': 0 + + which is actually shorthand for: + + 'user_id': reference_data['users'][0]['id'] + + This method converts the shorthand version into the full reference. + + """ + expanded_key = '%s_id' % key + reference_index = '%ss' % key + index_value = ( + reference_data[reference_index][shorthand_data[key]]['id']) + return expanded_key, index_value + + def create_implied_roles(self, implied_pattern, test_data): + """Create the implied roles specified in the test plan.""" + for implied_spec in implied_pattern: + # Each implied role specification is a dict of the form: + # + # {'role': 0, 'implied_roles': list of roles} + + prior_role = test_data['roles'][implied_spec['role']]['id'] + if isinstance(implied_spec['implied_roles'], list): + for this_role in implied_spec['implied_roles']: + implied_role = test_data['roles'][this_role]['id'] + self.role_api.create_implied_role(prior_role, implied_role) + else: + implied_role = ( + test_data['roles'][implied_spec['implied_roles']]['id']) + self.role_api.create_implied_role(prior_role, implied_role) + + def create_group_memberships(self, group_pattern, test_data): + """Create the group memberships specified in the test plan.""" + for group_spec in group_pattern: + # Each membership specification is a dict of the form: + # + # {'group': 0, 'users': [list of user indexes]} + # + # Add all users in the list to the specified group, first + # converting from index to full entity ID. + group_value = test_data['groups'][group_spec['group']]['id'] + for user_index in group_spec['users']: + user_value = test_data['users'][user_index]['id'] + self.identity_api.add_user_to_group(user_value, group_value) + return test_data + + def create_assignments(self, assignment_pattern, test_data): + """Create the assignments specified in the test plan.""" + # First store how many assignments are already in the system, + # so during the tests we can check the number of new assignments + # created. + test_data['initial_assignment_count'] = ( + len(self.assignment_api.list_role_assignments())) + + # Now create the new assignments in the test plan + for assignment in assignment_pattern: + # Each assignment is a dict of the form: + # + # { 'user': 0, 'project':1, 'role': 6} + # + # where the value of each item is the index into the array of + # entities created earlier. + # + # We process the assignment dict to create the args required to + # make the create_grant() call. + args = {} + for param in assignment: + if param == 'inherited_to_projects': + args[param] = assignment[param] + else: + # Turn 'entity : 0' into 'entity_id = ac6736ba873d' + # where entity in user, group, project or domain + key, value = self._convert_entity_shorthand( + param, assignment, test_data) + args[key] = value + self.assignment_api.create_grant(**args) + return test_data + + def execute_assignment_cases(self, test_plan, test_data): + """Execute the test plan, based on the created test_data.""" + def check_results(expected, actual, param_arg_count): + if param_arg_count == 0: + # It was an unfiltered call, so default fixture assignments + # might be polluting our answer - so we take into account + # how many assignments there were before the test. + self.assertEqual( + len(expected) + test_data['initial_assignment_count'], + len(actual)) + else: + self.assertThat(actual, matchers.HasLength(len(expected))) + + for each_expected in expected: + expected_assignment = {} + for param in each_expected: + if param == 'inherited_to_projects': + expected_assignment[param] = each_expected[param] + elif param == 'indirect': + # We're expecting the result to contain an indirect + # dict with the details how the role came to be placed + # on this entity - so convert the key/value pairs of + # that dict into real entity references. + indirect_term = {} + for indirect_param in each_expected[param]: + key, value = self._convert_entity_shorthand( + indirect_param, each_expected[param], + test_data) + indirect_term[key] = value + expected_assignment[param] = indirect_term + else: + # Convert a simple shorthand entry into a full + # entity reference + key, value = self._convert_entity_shorthand( + param, each_expected, test_data) + expected_assignment[key] = value + self.assertIn(expected_assignment, actual) + + def convert_group_ids_sourced_from_list(index_list, reference_data): + value_list = [] + for group_index in index_list: + value_list.append( + reference_data['groups'][group_index]['id']) + return value_list + + # Go through each test in the array, processing the input params, which + # we build into an args dict, and then call list_role_assignments. Then + # check the results against those specified in the test plan. + for test in test_plan.get('tests', []): + args = {} + for param in test['params']: + if param in ['effective', 'inherited', 'include_subtree']: + # Just pass the value into the args + args[param] = test['params'][param] + elif param == 'source_from_group_ids': + # Convert the list of indexes into a list of IDs + args[param] = convert_group_ids_sourced_from_list( + test['params']['source_from_group_ids'], test_data) + else: + # Turn 'entity : 0' into 'entity_id = ac6736ba873d' + # where entity in user, group, project or domain + key, value = self._convert_entity_shorthand( + param, test['params'], test_data) + args[key] = value + results = self.assignment_api.list_role_assignments(**args) + check_results(test['results'], results, len(args)) + + def execute_assignment_plan(self, test_plan): + """Create entities, assignments and execute the test plan. + + The standard method to call to create entities and assignments and + execute the tests as specified in the test_plan. The test_data + dict is returned so that, if required, the caller can execute + additional manual tests with the entities and assignments created. + + """ + test_data = self.create_entities(test_plan['entities']) + if 'implied_roles' in test_plan: + self.create_implied_roles(test_plan['implied_roles'], test_data) + if 'group_memberships' in test_plan: + self.create_group_memberships(test_plan['group_memberships'], + test_data) + if 'assignments' in test_plan: + test_data = self.create_assignments(test_plan['assignments'], + test_data) + self.execute_assignment_cases(test_plan, test_data) + return test_data + + +class AssignmentTests(AssignmentTestHelperMixin): + + def _get_domain_fixture(self): + domain = unit.new_domain_ref() + self.resource_api.create_domain(domain['id'], domain) + return domain + + def test_project_add_and_remove_user_role(self): + user_ids = self.assignment_api.list_user_ids_for_project( + self.tenant_bar['id']) + self.assertNotIn(self.user_two['id'], user_ids) + + self.assignment_api.add_role_to_user_and_project( + tenant_id=self.tenant_bar['id'], + user_id=self.user_two['id'], + role_id=self.role_other['id']) + user_ids = self.assignment_api.list_user_ids_for_project( + self.tenant_bar['id']) + self.assertIn(self.user_two['id'], user_ids) + + self.assignment_api.remove_role_from_user_and_project( + tenant_id=self.tenant_bar['id'], + user_id=self.user_two['id'], + role_id=self.role_other['id']) + + user_ids = self.assignment_api.list_user_ids_for_project( + self.tenant_bar['id']) + self.assertNotIn(self.user_two['id'], user_ids) + + def test_remove_user_role_not_assigned(self): + # Expect failure if attempt to remove a role that was never assigned to + # the user. + self.assertRaises(exception.RoleNotFound, + self.assignment_api. + remove_role_from_user_and_project, + tenant_id=self.tenant_bar['id'], + user_id=self.user_two['id'], + role_id=self.role_other['id']) + + def test_list_user_ids_for_project(self): + user_ids = self.assignment_api.list_user_ids_for_project( + self.tenant_baz['id']) + self.assertEqual(2, len(user_ids)) + self.assertIn(self.user_two['id'], user_ids) + self.assertIn(self.user_badguy['id'], user_ids) + + def test_list_user_ids_for_project_no_duplicates(self): + # Create user + user_ref = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user_ref = self.identity_api.create_user(user_ref) + # Create project + project_ref = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + self.resource_api.create_project( + project_ref['id'], project_ref) + # Create 2 roles and give user each role in project + for i in range(2): + role_ref = unit.new_role_ref() + self.role_api.create_role(role_ref['id'], role_ref) + self.assignment_api.add_role_to_user_and_project( + user_id=user_ref['id'], + tenant_id=project_ref['id'], + role_id=role_ref['id']) + # Get the list of user_ids in project + user_ids = self.assignment_api.list_user_ids_for_project( + project_ref['id']) + # Ensure the user is only returned once + self.assertEqual(1, len(user_ids)) + + def test_get_project_user_ids_returns_not_found(self): + self.assertRaises(exception.ProjectNotFound, + self.assignment_api.list_user_ids_for_project, + uuid.uuid4().hex) + + def test_list_role_assignments_unfiltered(self): + """Test unfiltered listing of role assignments.""" + test_plan = { + # Create a domain, with a user, group & project + 'entities': {'domains': {'users': 1, 'groups': 1, 'projects': 1}, + 'roles': 3}, + # Create a grant of each type (user/group on project/domain) + 'assignments': [{'user': 0, 'role': 0, 'domain': 0}, + {'user': 0, 'role': 1, 'project': 0}, + {'group': 0, 'role': 2, 'domain': 0}, + {'group': 0, 'role': 2, 'project': 0}], + 'tests': [ + # Check that we get back the 4 assignments + {'params': {}, + 'results': [{'user': 0, 'role': 0, 'domain': 0}, + {'user': 0, 'role': 1, 'project': 0}, + {'group': 0, 'role': 2, 'domain': 0}, + {'group': 0, 'role': 2, 'project': 0}]} + ] + } + self.execute_assignment_plan(test_plan) + + def test_list_role_assignments_filtered_by_role(self): + """Test listing of role assignments filtered by role ID.""" + test_plan = { + # Create a user, group & project in the default domain + 'entities': {'domains': {'id': CONF.identity.default_domain_id, + 'users': 1, 'groups': 1, 'projects': 1}, + 'roles': 3}, + # Create a grant of each type (user/group on project/domain) + 'assignments': [{'user': 0, 'role': 0, 'domain': 0}, + {'user': 0, 'role': 1, 'project': 0}, + {'group': 0, 'role': 2, 'domain': 0}, + {'group': 0, 'role': 2, 'project': 0}], + 'tests': [ + # Check that when filtering by role, we only get back those + # that match + {'params': {'role': 2}, + 'results': [{'group': 0, 'role': 2, 'domain': 0}, + {'group': 0, 'role': 2, 'project': 0}]} + ] + } + self.execute_assignment_plan(test_plan) + + def test_list_group_role_assignment(self): + # When a group role assignment is created and the role assignments are + # listed then the group role assignment is included in the list. + + test_plan = { + 'entities': {'domains': {'id': CONF.identity.default_domain_id, + 'groups': 1, 'projects': 1}, + 'roles': 1}, + 'assignments': [{'group': 0, 'role': 0, 'project': 0}], + 'tests': [ + {'params': {}, + 'results': [{'group': 0, 'role': 0, 'project': 0}]} + ] + } + self.execute_assignment_plan(test_plan) + + def test_list_role_assignments_bad_role(self): + assignment_list = self.assignment_api.list_role_assignments( + role_id=uuid.uuid4().hex) + self.assertEqual([], assignment_list) + + def test_add_duplicate_role_grant(self): + roles_ref = self.assignment_api.get_roles_for_user_and_project( + self.user_foo['id'], self.tenant_bar['id']) + self.assertNotIn(self.role_admin['id'], roles_ref) + self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], self.tenant_bar['id'], self.role_admin['id']) + self.assertRaises(exception.Conflict, + self.assignment_api.add_role_to_user_and_project, + self.user_foo['id'], + self.tenant_bar['id'], + self.role_admin['id']) + + def test_get_role_by_user_and_project_with_user_in_group(self): + """Test for get role by user and project, user was added into a group. + + Test Plan: + + - Create a user, a project & a group, add this user to group + - Create roles and grant them to user and project + - Check the role list get by the user and project was as expected + + """ + user_ref = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user_ref = self.identity_api.create_user(user_ref) + + project_ref = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + self.resource_api.create_project(project_ref['id'], project_ref) + + group = unit.new_group_ref(domain_id=CONF.identity.default_domain_id) + group_id = self.identity_api.create_group(group)['id'] + self.identity_api.add_user_to_group(user_ref['id'], group_id) + + role_ref_list = [] + for i in range(2): + role_ref = unit.new_role_ref() + self.role_api.create_role(role_ref['id'], role_ref) + role_ref_list.append(role_ref) + + self.assignment_api.add_role_to_user_and_project( + user_id=user_ref['id'], + tenant_id=project_ref['id'], + role_id=role_ref['id']) + + role_list = self.assignment_api.get_roles_for_user_and_project( + user_ref['id'], + project_ref['id']) + + self.assertEqual(set([r['id'] for r in role_ref_list]), + set(role_list)) + + def test_get_role_by_user_and_project(self): + roles_ref = self.assignment_api.get_roles_for_user_and_project( + self.user_foo['id'], self.tenant_bar['id']) + self.assertNotIn(self.role_admin['id'], roles_ref) + self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], self.tenant_bar['id'], self.role_admin['id']) + roles_ref = self.assignment_api.get_roles_for_user_and_project( + self.user_foo['id'], self.tenant_bar['id']) + self.assertIn(self.role_admin['id'], roles_ref) + self.assertNotIn('member', roles_ref) + + self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], self.tenant_bar['id'], 'member') + roles_ref = self.assignment_api.get_roles_for_user_and_project( + self.user_foo['id'], self.tenant_bar['id']) + self.assertIn(self.role_admin['id'], roles_ref) + self.assertIn('member', roles_ref) + + def test_get_roles_for_user_and_domain(self): + """Test for getting roles for user on a domain. + + Test Plan: + + - Create a domain, with 2 users + - Check no roles yet exit + - Give user1 two roles on the domain, user2 one role + - Get roles on user1 and the domain - maybe sure we only + get back the 2 roles on user1 + - Delete both roles from user1 + - Check we get no roles back for user1 on domain + + """ + new_domain = unit.new_domain_ref() + self.resource_api.create_domain(new_domain['id'], new_domain) + new_user1 = unit.new_user_ref(domain_id=new_domain['id']) + new_user1 = self.identity_api.create_user(new_user1) + new_user2 = unit.new_user_ref(domain_id=new_domain['id']) + new_user2 = self.identity_api.create_user(new_user2) + roles_ref = self.assignment_api.list_grants( + user_id=new_user1['id'], + domain_id=new_domain['id']) + self.assertEqual(0, len(roles_ref)) + # Now create the grants (roles are defined in default_fixtures) + self.assignment_api.create_grant(user_id=new_user1['id'], + domain_id=new_domain['id'], + role_id='member') + self.assignment_api.create_grant(user_id=new_user1['id'], + domain_id=new_domain['id'], + role_id='other') + self.assignment_api.create_grant(user_id=new_user2['id'], + domain_id=new_domain['id'], + role_id='admin') + # Read back the roles for user1 on domain + roles_ids = self.assignment_api.get_roles_for_user_and_domain( + new_user1['id'], new_domain['id']) + self.assertEqual(2, len(roles_ids)) + self.assertIn(self.role_member['id'], roles_ids) + self.assertIn(self.role_other['id'], roles_ids) + + # Now delete both grants for user1 + self.assignment_api.delete_grant(user_id=new_user1['id'], + domain_id=new_domain['id'], + role_id='member') + self.assignment_api.delete_grant(user_id=new_user1['id'], + domain_id=new_domain['id'], + role_id='other') + roles_ref = self.assignment_api.list_grants( + user_id=new_user1['id'], + domain_id=new_domain['id']) + self.assertEqual(0, len(roles_ref)) + + def test_get_roles_for_user_and_domain_returns_not_found(self): + """Test errors raised when getting roles for user on a domain. + + Test Plan: + + - Check non-existing user gives UserNotFound + - Check non-existing domain gives DomainNotFound + + """ + new_domain = self._get_domain_fixture() + new_user1 = unit.new_user_ref(domain_id=new_domain['id']) + new_user1 = self.identity_api.create_user(new_user1) + + self.assertRaises(exception.UserNotFound, + self.assignment_api.get_roles_for_user_and_domain, + uuid.uuid4().hex, + new_domain['id']) + + self.assertRaises(exception.DomainNotFound, + self.assignment_api.get_roles_for_user_and_domain, + new_user1['id'], + uuid.uuid4().hex) + + def test_get_roles_for_user_and_project_returns_not_found(self): + self.assertRaises(exception.UserNotFound, + self.assignment_api.get_roles_for_user_and_project, + uuid.uuid4().hex, + self.tenant_bar['id']) + + self.assertRaises(exception.ProjectNotFound, + self.assignment_api.get_roles_for_user_and_project, + self.user_foo['id'], + uuid.uuid4().hex) + + def test_add_role_to_user_and_project_returns_not_found(self): + self.assertRaises(exception.ProjectNotFound, + self.assignment_api.add_role_to_user_and_project, + self.user_foo['id'], + uuid.uuid4().hex, + self.role_admin['id']) + + self.assertRaises(exception.RoleNotFound, + self.assignment_api.add_role_to_user_and_project, + self.user_foo['id'], + self.tenant_bar['id'], + uuid.uuid4().hex) + + def test_add_role_to_user_and_project_no_user(self): + # If add_role_to_user_and_project and the user doesn't exist, then + # no error. + user_id_not_exist = uuid.uuid4().hex + self.assignment_api.add_role_to_user_and_project( + user_id_not_exist, self.tenant_bar['id'], self.role_admin['id']) + + def test_remove_role_from_user_and_project(self): + self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], self.tenant_bar['id'], 'member') + self.assignment_api.remove_role_from_user_and_project( + self.user_foo['id'], self.tenant_bar['id'], 'member') + roles_ref = self.assignment_api.get_roles_for_user_and_project( + self.user_foo['id'], self.tenant_bar['id']) + self.assertNotIn('member', roles_ref) + self.assertRaises(exception.NotFound, + self.assignment_api. + remove_role_from_user_and_project, + self.user_foo['id'], + self.tenant_bar['id'], + 'member') + + def test_get_role_grant_by_user_and_project(self): + roles_ref = self.assignment_api.list_grants( + user_id=self.user_foo['id'], + project_id=self.tenant_bar['id']) + self.assertEqual(1, len(roles_ref)) + self.assignment_api.create_grant(user_id=self.user_foo['id'], + project_id=self.tenant_bar['id'], + role_id=self.role_admin['id']) + roles_ref = self.assignment_api.list_grants( + user_id=self.user_foo['id'], + project_id=self.tenant_bar['id']) + self.assertIn(self.role_admin['id'], + [role_ref['id'] for role_ref in roles_ref]) + + self.assignment_api.create_grant(user_id=self.user_foo['id'], + project_id=self.tenant_bar['id'], + role_id='member') + roles_ref = self.assignment_api.list_grants( + user_id=self.user_foo['id'], + project_id=self.tenant_bar['id']) + + roles_ref_ids = [] + for ref in roles_ref: + roles_ref_ids.append(ref['id']) + self.assertIn(self.role_admin['id'], roles_ref_ids) + self.assertIn('member', roles_ref_ids) + + def test_remove_role_grant_from_user_and_project(self): + self.assignment_api.create_grant(user_id=self.user_foo['id'], + project_id=self.tenant_baz['id'], + role_id='member') + roles_ref = self.assignment_api.list_grants( + user_id=self.user_foo['id'], + project_id=self.tenant_baz['id']) + self.assertDictEqual(self.role_member, roles_ref[0]) + + self.assignment_api.delete_grant(user_id=self.user_foo['id'], + project_id=self.tenant_baz['id'], + role_id='member') + roles_ref = self.assignment_api.list_grants( + user_id=self.user_foo['id'], + project_id=self.tenant_baz['id']) + self.assertEqual(0, len(roles_ref)) + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.delete_grant, + user_id=self.user_foo['id'], + project_id=self.tenant_baz['id'], + role_id='member') + + def test_get_role_assignment_by_project_not_found(self): + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.check_grant_role_id, + user_id=self.user_foo['id'], + project_id=self.tenant_baz['id'], + role_id='member') + + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.check_grant_role_id, + group_id=uuid.uuid4().hex, + project_id=self.tenant_baz['id'], + role_id='member') + + def test_get_role_assignment_by_domain_not_found(self): + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.check_grant_role_id, + user_id=self.user_foo['id'], + domain_id=self.domain_default['id'], + role_id='member') + + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.check_grant_role_id, + group_id=uuid.uuid4().hex, + domain_id=self.domain_default['id'], + role_id='member') + + def test_del_role_assignment_by_project_not_found(self): + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.delete_grant, + user_id=self.user_foo['id'], + project_id=self.tenant_baz['id'], + role_id='member') + + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.delete_grant, + group_id=uuid.uuid4().hex, + project_id=self.tenant_baz['id'], + role_id='member') + + def test_del_role_assignment_by_domain_not_found(self): + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.delete_grant, + user_id=self.user_foo['id'], + domain_id=self.domain_default['id'], + role_id='member') + + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.delete_grant, + group_id=uuid.uuid4().hex, + domain_id=self.domain_default['id'], + role_id='member') + + def test_get_and_remove_role_grant_by_group_and_project(self): + new_domain = unit.new_domain_ref() + self.resource_api.create_domain(new_domain['id'], new_domain) + new_group = unit.new_group_ref(domain_id=new_domain['id']) + new_group = self.identity_api.create_group(new_group) + new_user = unit.new_user_ref(domain_id=new_domain['id']) + new_user = self.identity_api.create_user(new_user) + self.identity_api.add_user_to_group(new_user['id'], + new_group['id']) + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + project_id=self.tenant_bar['id']) + self.assertEqual(0, len(roles_ref)) + self.assignment_api.create_grant(group_id=new_group['id'], + project_id=self.tenant_bar['id'], + role_id='member') + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + project_id=self.tenant_bar['id']) + self.assertDictEqual(self.role_member, roles_ref[0]) + + self.assignment_api.delete_grant(group_id=new_group['id'], + project_id=self.tenant_bar['id'], + role_id='member') + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + project_id=self.tenant_bar['id']) + self.assertEqual(0, len(roles_ref)) + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.delete_grant, + group_id=new_group['id'], + project_id=self.tenant_bar['id'], + role_id='member') + + def test_get_and_remove_role_grant_by_group_and_domain(self): + new_domain = unit.new_domain_ref() + self.resource_api.create_domain(new_domain['id'], new_domain) + new_group = unit.new_group_ref(domain_id=new_domain['id']) + new_group = self.identity_api.create_group(new_group) + new_user = unit.new_user_ref(domain_id=new_domain['id']) + new_user = self.identity_api.create_user(new_user) + self.identity_api.add_user_to_group(new_user['id'], + new_group['id']) + + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + domain_id=new_domain['id']) + self.assertEqual(0, len(roles_ref)) + + self.assignment_api.create_grant(group_id=new_group['id'], + domain_id=new_domain['id'], + role_id='member') + + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + domain_id=new_domain['id']) + self.assertDictEqual(self.role_member, roles_ref[0]) + + self.assignment_api.delete_grant(group_id=new_group['id'], + domain_id=new_domain['id'], + role_id='member') + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + domain_id=new_domain['id']) + self.assertEqual(0, len(roles_ref)) + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.delete_grant, + group_id=new_group['id'], + domain_id=new_domain['id'], + role_id='member') + + def test_get_and_remove_correct_role_grant_from_a_mix(self): + new_domain = unit.new_domain_ref() + self.resource_api.create_domain(new_domain['id'], new_domain) + new_project = unit.new_project_ref(domain_id=new_domain['id']) + self.resource_api.create_project(new_project['id'], new_project) + new_group = unit.new_group_ref(domain_id=new_domain['id']) + new_group = self.identity_api.create_group(new_group) + new_group2 = unit.new_group_ref(domain_id=new_domain['id']) + new_group2 = self.identity_api.create_group(new_group2) + new_user = unit.new_user_ref(domain_id=new_domain['id']) + new_user = self.identity_api.create_user(new_user) + new_user2 = unit.new_user_ref(domain_id=new_domain['id']) + new_user2 = self.identity_api.create_user(new_user2) + self.identity_api.add_user_to_group(new_user['id'], + new_group['id']) + # First check we have no grants + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + domain_id=new_domain['id']) + self.assertEqual(0, len(roles_ref)) + # Now add the grant we are going to test for, and some others as + # well just to make sure we get back the right one + self.assignment_api.create_grant(group_id=new_group['id'], + domain_id=new_domain['id'], + role_id='member') + + self.assignment_api.create_grant(group_id=new_group2['id'], + domain_id=new_domain['id'], + role_id=self.role_admin['id']) + self.assignment_api.create_grant(user_id=new_user2['id'], + domain_id=new_domain['id'], + role_id=self.role_admin['id']) + self.assignment_api.create_grant(group_id=new_group['id'], + project_id=new_project['id'], + role_id=self.role_admin['id']) + + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + domain_id=new_domain['id']) + self.assertDictEqual(self.role_member, roles_ref[0]) + + self.assignment_api.delete_grant(group_id=new_group['id'], + domain_id=new_domain['id'], + role_id='member') + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + domain_id=new_domain['id']) + self.assertEqual(0, len(roles_ref)) + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.delete_grant, + group_id=new_group['id'], + domain_id=new_domain['id'], + role_id='member') + + def test_get_and_remove_role_grant_by_user_and_domain(self): + new_domain = unit.new_domain_ref() + self.resource_api.create_domain(new_domain['id'], new_domain) + new_user = unit.new_user_ref(domain_id=new_domain['id']) + new_user = self.identity_api.create_user(new_user) + roles_ref = self.assignment_api.list_grants( + user_id=new_user['id'], + domain_id=new_domain['id']) + self.assertEqual(0, len(roles_ref)) + self.assignment_api.create_grant(user_id=new_user['id'], + domain_id=new_domain['id'], + role_id='member') + roles_ref = self.assignment_api.list_grants( + user_id=new_user['id'], + domain_id=new_domain['id']) + self.assertDictEqual(self.role_member, roles_ref[0]) + + self.assignment_api.delete_grant(user_id=new_user['id'], + domain_id=new_domain['id'], + role_id='member') + roles_ref = self.assignment_api.list_grants( + user_id=new_user['id'], + domain_id=new_domain['id']) + self.assertEqual(0, len(roles_ref)) + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.delete_grant, + user_id=new_user['id'], + domain_id=new_domain['id'], + role_id='member') + + def test_get_and_remove_role_grant_by_group_and_cross_domain(self): + group1_domain1_role = unit.new_role_ref() + self.role_api.create_role(group1_domain1_role['id'], + group1_domain1_role) + group1_domain2_role = unit.new_role_ref() + self.role_api.create_role(group1_domain2_role['id'], + group1_domain2_role) + domain1 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = unit.new_domain_ref() + self.resource_api.create_domain(domain2['id'], domain2) + group1 = unit.new_group_ref(domain_id=domain1['id']) + group1 = self.identity_api.create_group(group1) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + domain_id=domain1['id']) + self.assertEqual(0, len(roles_ref)) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + domain_id=domain2['id']) + self.assertEqual(0, len(roles_ref)) + self.assignment_api.create_grant(group_id=group1['id'], + domain_id=domain1['id'], + role_id=group1_domain1_role['id']) + self.assignment_api.create_grant(group_id=group1['id'], + domain_id=domain2['id'], + role_id=group1_domain2_role['id']) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + domain_id=domain1['id']) + self.assertDictEqual(group1_domain1_role, roles_ref[0]) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + domain_id=domain2['id']) + self.assertDictEqual(group1_domain2_role, roles_ref[0]) + + self.assignment_api.delete_grant(group_id=group1['id'], + domain_id=domain2['id'], + role_id=group1_domain2_role['id']) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + domain_id=domain2['id']) + self.assertEqual(0, len(roles_ref)) + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.delete_grant, + group_id=group1['id'], + domain_id=domain2['id'], + role_id=group1_domain2_role['id']) + + def test_get_and_remove_role_grant_by_user_and_cross_domain(self): + user1_domain1_role = unit.new_role_ref() + self.role_api.create_role(user1_domain1_role['id'], user1_domain1_role) + user1_domain2_role = unit.new_role_ref() + self.role_api.create_role(user1_domain2_role['id'], user1_domain2_role) + domain1 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = unit.new_domain_ref() + self.resource_api.create_domain(domain2['id'], domain2) + user1 = unit.new_user_ref(domain_id=domain1['id']) + user1 = self.identity_api.create_user(user1) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + domain_id=domain1['id']) + self.assertEqual(0, len(roles_ref)) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + domain_id=domain2['id']) + self.assertEqual(0, len(roles_ref)) + self.assignment_api.create_grant(user_id=user1['id'], + domain_id=domain1['id'], + role_id=user1_domain1_role['id']) + self.assignment_api.create_grant(user_id=user1['id'], + domain_id=domain2['id'], + role_id=user1_domain2_role['id']) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + domain_id=domain1['id']) + self.assertDictEqual(user1_domain1_role, roles_ref[0]) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + domain_id=domain2['id']) + self.assertDictEqual(user1_domain2_role, roles_ref[0]) + + self.assignment_api.delete_grant(user_id=user1['id'], + domain_id=domain2['id'], + role_id=user1_domain2_role['id']) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + domain_id=domain2['id']) + self.assertEqual(0, len(roles_ref)) + self.assertRaises(exception.RoleAssignmentNotFound, + self.assignment_api.delete_grant, + user_id=user1['id'], + domain_id=domain2['id'], + role_id=user1_domain2_role['id']) + + def test_role_grant_by_group_and_cross_domain_project(self): + role1 = unit.new_role_ref() + self.role_api.create_role(role1['id'], role1) + role2 = unit.new_role_ref() + self.role_api.create_role(role2['id'], role2) + domain1 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = unit.new_domain_ref() + self.resource_api.create_domain(domain2['id'], domain2) + group1 = unit.new_group_ref(domain_id=domain1['id']) + group1 = self.identity_api.create_group(group1) + project1 = unit.new_project_ref(domain_id=domain2['id']) + self.resource_api.create_project(project1['id'], project1) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + project_id=project1['id']) + self.assertEqual(0, len(roles_ref)) + self.assignment_api.create_grant(group_id=group1['id'], + project_id=project1['id'], + role_id=role1['id']) + self.assignment_api.create_grant(group_id=group1['id'], + project_id=project1['id'], + role_id=role2['id']) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + project_id=project1['id']) + + roles_ref_ids = [] + for ref in roles_ref: + roles_ref_ids.append(ref['id']) + self.assertIn(role1['id'], roles_ref_ids) + self.assertIn(role2['id'], roles_ref_ids) + + self.assignment_api.delete_grant(group_id=group1['id'], + project_id=project1['id'], + role_id=role1['id']) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + project_id=project1['id']) + self.assertEqual(1, len(roles_ref)) + self.assertDictEqual(role2, roles_ref[0]) + + def test_role_grant_by_user_and_cross_domain_project(self): + role1 = unit.new_role_ref() + self.role_api.create_role(role1['id'], role1) + role2 = unit.new_role_ref() + self.role_api.create_role(role2['id'], role2) + domain1 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = unit.new_domain_ref() + self.resource_api.create_domain(domain2['id'], domain2) + user1 = unit.new_user_ref(domain_id=domain1['id']) + user1 = self.identity_api.create_user(user1) + project1 = unit.new_project_ref(domain_id=domain2['id']) + self.resource_api.create_project(project1['id'], project1) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + project_id=project1['id']) + self.assertEqual(0, len(roles_ref)) + self.assignment_api.create_grant(user_id=user1['id'], + project_id=project1['id'], + role_id=role1['id']) + self.assignment_api.create_grant(user_id=user1['id'], + project_id=project1['id'], + role_id=role2['id']) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + project_id=project1['id']) + + roles_ref_ids = [] + for ref in roles_ref: + roles_ref_ids.append(ref['id']) + self.assertIn(role1['id'], roles_ref_ids) + self.assertIn(role2['id'], roles_ref_ids) + + self.assignment_api.delete_grant(user_id=user1['id'], + project_id=project1['id'], + role_id=role1['id']) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + project_id=project1['id']) + self.assertEqual(1, len(roles_ref)) + self.assertDictEqual(role2, roles_ref[0]) + + def test_delete_user_grant_no_user(self): + # Can delete a grant where the user doesn't exist. + role = unit.new_role_ref() + role_id = role['id'] + self.role_api.create_role(role_id, role) + + user_id = uuid.uuid4().hex + + self.assignment_api.create_grant(role_id, user_id=user_id, + project_id=self.tenant_bar['id']) + + self.assignment_api.delete_grant(role_id, user_id=user_id, + project_id=self.tenant_bar['id']) + + def test_delete_group_grant_no_group(self): + # Can delete a grant where the group doesn't exist. + role = unit.new_role_ref() + role_id = role['id'] + self.role_api.create_role(role_id, role) + + group_id = uuid.uuid4().hex + + self.assignment_api.create_grant(role_id, group_id=group_id, + project_id=self.tenant_bar['id']) + + self.assignment_api.delete_grant(role_id, group_id=group_id, + project_id=self.tenant_bar['id']) + + def test_grant_crud_throws_exception_if_invalid_role(self): + """Ensure RoleNotFound thrown if role does not exist.""" + def assert_role_not_found_exception(f, **kwargs): + self.assertRaises(exception.RoleNotFound, f, + role_id=uuid.uuid4().hex, **kwargs) + + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user_resp = self.identity_api.create_user(user) + group = unit.new_group_ref(domain_id=CONF.identity.default_domain_id) + group_resp = self.identity_api.create_group(group) + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + project_resp = self.resource_api.create_project(project['id'], project) + + for manager_call in [self.assignment_api.create_grant, + self.assignment_api.get_grant, + self.assignment_api.delete_grant]: + assert_role_not_found_exception( + manager_call, + user_id=user_resp['id'], project_id=project_resp['id']) + assert_role_not_found_exception( + manager_call, + group_id=group_resp['id'], project_id=project_resp['id']) + assert_role_not_found_exception( + manager_call, + user_id=user_resp['id'], + domain_id=CONF.identity.default_domain_id) + assert_role_not_found_exception( + manager_call, + group_id=group_resp['id'], + domain_id=CONF.identity.default_domain_id) + + def test_multi_role_grant_by_user_group_on_project_domain(self): + role_list = [] + for _ in range(10): + role = unit.new_role_ref() + self.role_api.create_role(role['id'], role) + role_list.append(role) + domain1 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + user1 = unit.new_user_ref(domain_id=domain1['id']) + user1 = self.identity_api.create_user(user1) + group1 = unit.new_group_ref(domain_id=domain1['id']) + group1 = self.identity_api.create_group(group1) + group2 = unit.new_group_ref(domain_id=domain1['id']) + group2 = self.identity_api.create_group(group2) + project1 = unit.new_project_ref(domain_id=domain1['id']) + self.resource_api.create_project(project1['id'], project1) + + self.identity_api.add_user_to_group(user1['id'], + group1['id']) + self.identity_api.add_user_to_group(user1['id'], + group2['id']) + + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + project_id=project1['id']) + self.assertEqual(0, len(roles_ref)) + self.assignment_api.create_grant(user_id=user1['id'], + domain_id=domain1['id'], + role_id=role_list[0]['id']) + self.assignment_api.create_grant(user_id=user1['id'], + domain_id=domain1['id'], + role_id=role_list[1]['id']) + self.assignment_api.create_grant(group_id=group1['id'], + domain_id=domain1['id'], + role_id=role_list[2]['id']) + self.assignment_api.create_grant(group_id=group1['id'], + domain_id=domain1['id'], + role_id=role_list[3]['id']) + self.assignment_api.create_grant(user_id=user1['id'], + project_id=project1['id'], + role_id=role_list[4]['id']) + self.assignment_api.create_grant(user_id=user1['id'], + project_id=project1['id'], + role_id=role_list[5]['id']) + self.assignment_api.create_grant(group_id=group1['id'], + project_id=project1['id'], + role_id=role_list[6]['id']) + self.assignment_api.create_grant(group_id=group1['id'], + project_id=project1['id'], + role_id=role_list[7]['id']) + roles_ref = self.assignment_api.list_grants(user_id=user1['id'], + domain_id=domain1['id']) + self.assertEqual(2, len(roles_ref)) + self.assertIn(role_list[0], roles_ref) + self.assertIn(role_list[1], roles_ref) + roles_ref = self.assignment_api.list_grants(group_id=group1['id'], + domain_id=domain1['id']) + self.assertEqual(2, len(roles_ref)) + self.assertIn(role_list[2], roles_ref) + self.assertIn(role_list[3], roles_ref) + roles_ref = self.assignment_api.list_grants(user_id=user1['id'], + project_id=project1['id']) + self.assertEqual(2, len(roles_ref)) + self.assertIn(role_list[4], roles_ref) + self.assertIn(role_list[5], roles_ref) + roles_ref = self.assignment_api.list_grants(group_id=group1['id'], + project_id=project1['id']) + self.assertEqual(2, len(roles_ref)) + self.assertIn(role_list[6], roles_ref) + self.assertIn(role_list[7], roles_ref) + + # Now test the alternate way of getting back lists of grants, + # where user and group roles are combined. These should match + # the above results. + combined_list = self.assignment_api.get_roles_for_user_and_project( + user1['id'], project1['id']) + self.assertEqual(4, len(combined_list)) + self.assertIn(role_list[4]['id'], combined_list) + self.assertIn(role_list[5]['id'], combined_list) + self.assertIn(role_list[6]['id'], combined_list) + self.assertIn(role_list[7]['id'], combined_list) + + combined_role_list = self.assignment_api.get_roles_for_user_and_domain( + user1['id'], domain1['id']) + self.assertEqual(4, len(combined_role_list)) + self.assertIn(role_list[0]['id'], combined_role_list) + self.assertIn(role_list[1]['id'], combined_role_list) + self.assertIn(role_list[2]['id'], combined_role_list) + self.assertIn(role_list[3]['id'], combined_role_list) + + def test_multi_group_grants_on_project_domain(self): + """Test multiple group roles for user on project and domain. + + Test Plan: + + - Create 6 roles + - Create a domain, with a project, user and two groups + - Make the user a member of both groups + - Check no roles yet exit + - Assign a role to each user and both groups on both the + project and domain + - Get a list of effective roles for the user on both the + project and domain, checking we get back the correct three + roles + + """ + role_list = [] + for _ in range(6): + role = unit.new_role_ref() + self.role_api.create_role(role['id'], role) + role_list.append(role) + domain1 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + user1 = unit.new_user_ref(domain_id=domain1['id']) + user1 = self.identity_api.create_user(user1) + group1 = unit.new_group_ref(domain_id=domain1['id']) + group1 = self.identity_api.create_group(group1) + group2 = unit.new_group_ref(domain_id=domain1['id']) + group2 = self.identity_api.create_group(group2) + project1 = unit.new_project_ref(domain_id=domain1['id']) + self.resource_api.create_project(project1['id'], project1) + + self.identity_api.add_user_to_group(user1['id'], + group1['id']) + self.identity_api.add_user_to_group(user1['id'], + group2['id']) + + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + project_id=project1['id']) + self.assertEqual(0, len(roles_ref)) + self.assignment_api.create_grant(user_id=user1['id'], + domain_id=domain1['id'], + role_id=role_list[0]['id']) + self.assignment_api.create_grant(group_id=group1['id'], + domain_id=domain1['id'], + role_id=role_list[1]['id']) + self.assignment_api.create_grant(group_id=group2['id'], + domain_id=domain1['id'], + role_id=role_list[2]['id']) + self.assignment_api.create_grant(user_id=user1['id'], + project_id=project1['id'], + role_id=role_list[3]['id']) + self.assignment_api.create_grant(group_id=group1['id'], + project_id=project1['id'], + role_id=role_list[4]['id']) + self.assignment_api.create_grant(group_id=group2['id'], + project_id=project1['id'], + role_id=role_list[5]['id']) + + # Read by the roles, ensuring we get the correct 3 roles for + # both project and domain + combined_list = self.assignment_api.get_roles_for_user_and_project( + user1['id'], project1['id']) + self.assertEqual(3, len(combined_list)) + self.assertIn(role_list[3]['id'], combined_list) + self.assertIn(role_list[4]['id'], combined_list) + self.assertIn(role_list[5]['id'], combined_list) + + combined_role_list = self.assignment_api.get_roles_for_user_and_domain( + user1['id'], domain1['id']) + self.assertEqual(3, len(combined_role_list)) + self.assertIn(role_list[0]['id'], combined_role_list) + self.assertIn(role_list[1]['id'], combined_role_list) + self.assertIn(role_list[2]['id'], combined_role_list) + + def test_delete_role_with_user_and_group_grants(self): + role1 = unit.new_role_ref() + self.role_api.create_role(role1['id'], role1) + domain1 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + project1 = unit.new_project_ref(domain_id=domain1['id']) + self.resource_api.create_project(project1['id'], project1) + user1 = unit.new_user_ref(domain_id=domain1['id']) + user1 = self.identity_api.create_user(user1) + group1 = unit.new_group_ref(domain_id=domain1['id']) + group1 = self.identity_api.create_group(group1) + self.assignment_api.create_grant(user_id=user1['id'], + project_id=project1['id'], + role_id=role1['id']) + self.assignment_api.create_grant(user_id=user1['id'], + domain_id=domain1['id'], + role_id=role1['id']) + self.assignment_api.create_grant(group_id=group1['id'], + project_id=project1['id'], + role_id=role1['id']) + self.assignment_api.create_grant(group_id=group1['id'], + domain_id=domain1['id'], + role_id=role1['id']) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + project_id=project1['id']) + self.assertEqual(1, len(roles_ref)) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + project_id=project1['id']) + self.assertEqual(1, len(roles_ref)) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + domain_id=domain1['id']) + self.assertEqual(1, len(roles_ref)) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + domain_id=domain1['id']) + self.assertEqual(1, len(roles_ref)) + self.role_api.delete_role(role1['id']) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + project_id=project1['id']) + self.assertEqual(0, len(roles_ref)) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + project_id=project1['id']) + self.assertEqual(0, len(roles_ref)) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + domain_id=domain1['id']) + self.assertEqual(0, len(roles_ref)) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + domain_id=domain1['id']) + self.assertEqual(0, len(roles_ref)) + + def test_list_role_assignment_by_domain(self): + """Test listing of role assignment filtered by domain.""" + test_plan = { + # A domain with 3 users, 1 group, a spoiler domain and 2 roles. + 'entities': {'domains': [{'users': 3, 'groups': 1}, 1], + 'roles': 2}, + # Users 1 & 2 are in the group + 'group_memberships': [{'group': 0, 'users': [1, 2]}], + # Assign a role for user 0 and the group + 'assignments': [{'user': 0, 'role': 0, 'domain': 0}, + {'group': 0, 'role': 1, 'domain': 0}], + 'tests': [ + # List all effective assignments for domain[0]. + # Should get one direct user role and user roles for each of + # the users in the group. + {'params': {'domain': 0, 'effective': True}, + 'results': [{'user': 0, 'role': 0, 'domain': 0}, + {'user': 1, 'role': 1, 'domain': 0, + 'indirect': {'group': 0}}, + {'user': 2, 'role': 1, 'domain': 0, + 'indirect': {'group': 0}} + ]}, + # Using domain[1] should return nothing + {'params': {'domain': 1, 'effective': True}, + 'results': []}, + ] + } + self.execute_assignment_plan(test_plan) + + def test_list_role_assignment_by_user_with_domain_group_roles(self): + """Test listing assignments by user, with group roles on a domain.""" + test_plan = { + # A domain with 3 users, 3 groups, a spoiler domain + # plus 3 roles. + 'entities': {'domains': [{'users': 3, 'groups': 3}, 1], + 'roles': 3}, + # Users 1 & 2 are in the group 0, User 1 also in group 1 + 'group_memberships': [{'group': 0, 'users': [0, 1]}, + {'group': 1, 'users': [0]}], + 'assignments': [{'user': 0, 'role': 0, 'domain': 0}, + {'group': 0, 'role': 1, 'domain': 0}, + {'group': 1, 'role': 2, 'domain': 0}, + # ...and two spoiler assignments + {'user': 1, 'role': 1, 'domain': 0}, + {'group': 2, 'role': 2, 'domain': 0}], + 'tests': [ + # List all effective assignments for user[0]. + # Should get one direct user role and a user roles for each of + # groups 0 and 1 + {'params': {'user': 0, 'effective': True}, + 'results': [{'user': 0, 'role': 0, 'domain': 0}, + {'user': 0, 'role': 1, 'domain': 0, + 'indirect': {'group': 0}}, + {'user': 0, 'role': 2, 'domain': 0, + 'indirect': {'group': 1}} + ]}, + # Adding domain[0] as a filter should return the same data + {'params': {'user': 0, 'domain': 0, 'effective': True}, + 'results': [{'user': 0, 'role': 0, 'domain': 0}, + {'user': 0, 'role': 1, 'domain': 0, + 'indirect': {'group': 0}}, + {'user': 0, 'role': 2, 'domain': 0, + 'indirect': {'group': 1}} + ]}, + # Using domain[1] should return nothing + {'params': {'user': 0, 'domain': 1, 'effective': True}, + 'results': []}, + # Using user[2] should return nothing + {'params': {'user': 2, 'domain': 0, 'effective': True}, + 'results': []}, + ] + } + self.execute_assignment_plan(test_plan) + + def test_list_role_assignment_using_sourced_groups(self): + """Test listing assignments when restricted by source groups.""" + test_plan = { + # The default domain with 3 users, 3 groups, 3 projects, + # plus 3 roles. + 'entities': {'domains': {'id': CONF.identity.default_domain_id, + 'users': 3, 'groups': 3, 'projects': 3}, + 'roles': 3}, + # Users 0 & 1 are in the group 0, User 0 also in group 1 + 'group_memberships': [{'group': 0, 'users': [0, 1]}, + {'group': 1, 'users': [0]}], + # Spread the assignments around - we want to be able to show that + # if sourced by group, assignments from other sources are excluded + 'assignments': [{'user': 0, 'role': 0, 'project': 0}, + {'group': 0, 'role': 1, 'project': 1}, + {'group': 1, 'role': 2, 'project': 0}, + {'group': 1, 'role': 2, 'project': 1}, + {'user': 2, 'role': 1, 'project': 1}, + {'group': 2, 'role': 2, 'project': 2} + ], + 'tests': [ + # List all effective assignments sourced from groups 0 and 1 + {'params': {'source_from_group_ids': [0, 1], + 'effective': True}, + 'results': [{'group': 0, 'role': 1, 'project': 1}, + {'group': 1, 'role': 2, 'project': 0}, + {'group': 1, 'role': 2, 'project': 1} + ]}, + # Adding a role a filter should further restrict the entries + {'params': {'source_from_group_ids': [0, 1], 'role': 2, + 'effective': True}, + 'results': [{'group': 1, 'role': 2, 'project': 0}, + {'group': 1, 'role': 2, 'project': 1} + ]}, + ] + } + self.execute_assignment_plan(test_plan) + + def test_list_role_assignment_using_sourced_groups_with_domains(self): + """Test listing domain assignments when restricted by source groups.""" + test_plan = { + # A domain with 3 users, 3 groups, 3 projects, a second domain, + # plus 3 roles. + 'entities': {'domains': [{'users': 3, 'groups': 3, 'projects': 3}, + 1], + 'roles': 3}, + # Users 0 & 1 are in the group 0, User 0 also in group 1 + 'group_memberships': [{'group': 0, 'users': [0, 1]}, + {'group': 1, 'users': [0]}], + # Spread the assignments around - we want to be able to show that + # if sourced by group, assignments from other sources are excluded + 'assignments': [{'user': 0, 'role': 0, 'domain': 0}, + {'group': 0, 'role': 1, 'domain': 1}, + {'group': 1, 'role': 2, 'project': 0}, + {'group': 1, 'role': 2, 'project': 1}, + {'user': 2, 'role': 1, 'project': 1}, + {'group': 2, 'role': 2, 'project': 2} + ], + 'tests': [ + # List all effective assignments sourced from groups 0 and 1 + {'params': {'source_from_group_ids': [0, 1], + 'effective': True}, + 'results': [{'group': 0, 'role': 1, 'domain': 1}, + {'group': 1, 'role': 2, 'project': 0}, + {'group': 1, 'role': 2, 'project': 1} + ]}, + # Adding a role a filter should further restrict the entries + {'params': {'source_from_group_ids': [0, 1], 'role': 1, + 'effective': True}, + 'results': [{'group': 0, 'role': 1, 'domain': 1}, + ]}, + ] + } + self.execute_assignment_plan(test_plan) + + def test_list_role_assignment_fails_with_userid_and_source_groups(self): + """Show we trap this unsupported internal combination of params.""" + group = unit.new_group_ref(domain_id=CONF.identity.default_domain_id) + group = self.identity_api.create_group(group) + self.assertRaises(exception.UnexpectedError, + self.assignment_api.list_role_assignments, + effective=True, + user_id=self.user_foo['id'], + source_from_group_ids=[group['id']]) + + def test_add_user_to_project(self): + self.assignment_api.add_user_to_project(self.tenant_baz['id'], + self.user_foo['id']) + tenants = self.assignment_api.list_projects_for_user( + self.user_foo['id']) + self.assertIn(self.tenant_baz, tenants) + + def test_add_user_to_project_missing_default_role(self): + self.role_api.delete_role(CONF.member_role_id) + self.assertRaises(exception.RoleNotFound, + self.role_api.get_role, + CONF.member_role_id) + self.assignment_api.add_user_to_project(self.tenant_baz['id'], + self.user_foo['id']) + tenants = ( + self.assignment_api.list_projects_for_user(self.user_foo['id'])) + self.assertIn(self.tenant_baz, tenants) + default_role = self.role_api.get_role(CONF.member_role_id) + self.assertIsNotNone(default_role) + + def test_add_user_to_project_returns_not_found(self): + self.assertRaises(exception.ProjectNotFound, + self.assignment_api.add_user_to_project, + uuid.uuid4().hex, + self.user_foo['id']) + + def test_add_user_to_project_no_user(self): + # If add_user_to_project and the user doesn't exist, then + # no error. + user_id_not_exist = uuid.uuid4().hex + self.assignment_api.add_user_to_project(self.tenant_bar['id'], + user_id_not_exist) + + def test_remove_user_from_project(self): + self.assignment_api.add_user_to_project(self.tenant_baz['id'], + self.user_foo['id']) + self.assignment_api.remove_user_from_project(self.tenant_baz['id'], + self.user_foo['id']) + tenants = self.assignment_api.list_projects_for_user( + self.user_foo['id']) + self.assertNotIn(self.tenant_baz, tenants) + + def test_remove_user_from_project_race_delete_role(self): + self.assignment_api.add_user_to_project(self.tenant_baz['id'], + self.user_foo['id']) + self.assignment_api.add_role_to_user_and_project( + tenant_id=self.tenant_baz['id'], + user_id=self.user_foo['id'], + role_id=self.role_other['id']) + + # Mock a race condition, delete a role after + # get_roles_for_user_and_project() is called in + # remove_user_from_project(). + roles = self.assignment_api.get_roles_for_user_and_project( + self.user_foo['id'], self.tenant_baz['id']) + self.role_api.delete_role(self.role_other['id']) + self.assignment_api.get_roles_for_user_and_project = mock.Mock( + return_value=roles) + self.assignment_api.remove_user_from_project(self.tenant_baz['id'], + self.user_foo['id']) + tenants = self.assignment_api.list_projects_for_user( + self.user_foo['id']) + self.assertNotIn(self.tenant_baz, tenants) + + def test_remove_user_from_project_returns_not_found(self): + self.assertRaises(exception.ProjectNotFound, + self.assignment_api.remove_user_from_project, + uuid.uuid4().hex, + self.user_foo['id']) + + self.assertRaises(exception.UserNotFound, + self.assignment_api.remove_user_from_project, + self.tenant_bar['id'], + uuid.uuid4().hex) + + self.assertRaises(exception.NotFound, + self.assignment_api.remove_user_from_project, + self.tenant_baz['id'], + self.user_foo['id']) + + def test_list_user_project_ids_returns_not_found(self): + self.assertRaises(exception.UserNotFound, + self.assignment_api.list_projects_for_user, + uuid.uuid4().hex) + + def test_delete_user_with_project_association(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user = self.identity_api.create_user(user) + self.assignment_api.add_user_to_project(self.tenant_bar['id'], + user['id']) + self.identity_api.delete_user(user['id']) + self.assertRaises(exception.UserNotFound, + self.assignment_api.list_projects_for_user, + user['id']) + + def test_delete_user_with_project_roles(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user = self.identity_api.create_user(user) + self.assignment_api.add_role_to_user_and_project( + user['id'], + self.tenant_bar['id'], + self.role_member['id']) + self.identity_api.delete_user(user['id']) + self.assertRaises(exception.UserNotFound, + self.assignment_api.list_projects_for_user, + user['id']) + + def test_delete_role_returns_not_found(self): + self.assertRaises(exception.RoleNotFound, + self.role_api.delete_role, + uuid.uuid4().hex) + + def test_delete_project_with_role_assignments(self): + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + self.resource_api.create_project(project['id'], project) + self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], project['id'], 'member') + self.resource_api.delete_project(project['id']) + self.assertRaises(exception.ProjectNotFound, + self.assignment_api.list_user_ids_for_project, + project['id']) + + def test_delete_role_check_role_grant(self): + role = unit.new_role_ref() + alt_role = unit.new_role_ref() + self.role_api.create_role(role['id'], role) + self.role_api.create_role(alt_role['id'], alt_role) + self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], self.tenant_bar['id'], role['id']) + self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], self.tenant_bar['id'], alt_role['id']) + self.role_api.delete_role(role['id']) + roles_ref = self.assignment_api.get_roles_for_user_and_project( + self.user_foo['id'], self.tenant_bar['id']) + self.assertNotIn(role['id'], roles_ref) + self.assertIn(alt_role['id'], roles_ref) + + def test_list_projects_for_user(self): + domain = unit.new_domain_ref() + self.resource_api.create_domain(domain['id'], domain) + user1 = unit.new_user_ref(domain_id=domain['id']) + user1 = self.identity_api.create_user(user1) + user_projects = self.assignment_api.list_projects_for_user(user1['id']) + self.assertEqual(0, len(user_projects)) + self.assignment_api.create_grant(user_id=user1['id'], + project_id=self.tenant_bar['id'], + role_id=self.role_member['id']) + self.assignment_api.create_grant(user_id=user1['id'], + project_id=self.tenant_baz['id'], + role_id=self.role_member['id']) + user_projects = self.assignment_api.list_projects_for_user(user1['id']) + self.assertEqual(2, len(user_projects)) + + def test_list_projects_for_user_with_grants(self): + # Create two groups each with a role on a different project, and + # make user1 a member of both groups. Both these new projects + # should now be included, along with any direct user grants. + domain = unit.new_domain_ref() + self.resource_api.create_domain(domain['id'], domain) + user1 = unit.new_user_ref(domain_id=domain['id']) + user1 = self.identity_api.create_user(user1) + group1 = unit.new_group_ref(domain_id=domain['id']) + group1 = self.identity_api.create_group(group1) + group2 = unit.new_group_ref(domain_id=domain['id']) + group2 = self.identity_api.create_group(group2) + project1 = unit.new_project_ref(domain_id=domain['id']) + self.resource_api.create_project(project1['id'], project1) + project2 = unit.new_project_ref(domain_id=domain['id']) + self.resource_api.create_project(project2['id'], project2) + self.identity_api.add_user_to_group(user1['id'], group1['id']) + self.identity_api.add_user_to_group(user1['id'], group2['id']) + + # Create 3 grants, one user grant, the other two as group grants + self.assignment_api.create_grant(user_id=user1['id'], + project_id=self.tenant_bar['id'], + role_id=self.role_member['id']) + self.assignment_api.create_grant(group_id=group1['id'], + project_id=project1['id'], + role_id=self.role_admin['id']) + self.assignment_api.create_grant(group_id=group2['id'], + project_id=project2['id'], + role_id=self.role_admin['id']) + user_projects = self.assignment_api.list_projects_for_user(user1['id']) + self.assertEqual(3, len(user_projects)) + + def test_create_grant_no_user(self): + # If call create_grant with a user that doesn't exist, doesn't fail. + self.assignment_api.create_grant( + self.role_other['id'], + user_id=uuid.uuid4().hex, + project_id=self.tenant_bar['id']) + + def test_create_grant_no_group(self): + # If call create_grant with a group that doesn't exist, doesn't fail. + self.assignment_api.create_grant( + self.role_other['id'], + group_id=uuid.uuid4().hex, + project_id=self.tenant_bar['id']) + + def test_delete_group_removes_role_assignments(self): + # When a group is deleted any role assignments for the group are + # removed. + + MEMBER_ROLE_ID = 'member' + + def get_member_assignments(): + assignments = self.assignment_api.list_role_assignments() + return [x for x in assignments if x['role_id'] == MEMBER_ROLE_ID] + + orig_member_assignments = get_member_assignments() + + # Create a group. + new_group = unit.new_group_ref( + domain_id=CONF.identity.default_domain_id) + new_group = self.identity_api.create_group(new_group) + + # Create a project. + new_project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + self.resource_api.create_project(new_project['id'], new_project) + + # Assign a role to the group. + self.assignment_api.create_grant( + group_id=new_group['id'], project_id=new_project['id'], + role_id=MEMBER_ROLE_ID) + + # Delete the group. + self.identity_api.delete_group(new_group['id']) + + # Check that the role assignment for the group is gone + member_assignments = get_member_assignments() + + self.assertThat(member_assignments, + matchers.Equals(orig_member_assignments)) + + def test_get_roles_for_groups_on_domain(self): + """Test retrieving group domain roles. + + Test Plan: + + - Create a domain, three groups and three roles + - Assign one an inherited and the others a non-inherited group role + to the domain + - Ensure that only the non-inherited roles are returned on the domain + + """ + domain1 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + group_list = [] + group_id_list = [] + role_list = [] + for _ in range(3): + group = unit.new_group_ref(domain_id=domain1['id']) + group = self.identity_api.create_group(group) + group_list.append(group) + group_id_list.append(group['id']) + + role = unit.new_role_ref() + self.role_api.create_role(role['id'], role) + role_list.append(role) + + # Assign the roles - one is inherited + self.assignment_api.create_grant(group_id=group_list[0]['id'], + domain_id=domain1['id'], + role_id=role_list[0]['id']) + self.assignment_api.create_grant(group_id=group_list[1]['id'], + domain_id=domain1['id'], + role_id=role_list[1]['id']) + self.assignment_api.create_grant(group_id=group_list[2]['id'], + domain_id=domain1['id'], + role_id=role_list[2]['id'], + inherited_to_projects=True) + + # Now get the effective roles for the groups on the domain project. We + # shouldn't get back the inherited role. + + role_refs = self.assignment_api.get_roles_for_groups( + group_id_list, domain_id=domain1['id']) + + self.assertThat(role_refs, matchers.HasLength(2)) + self.assertIn(role_list[0], role_refs) + self.assertIn(role_list[1], role_refs) + + def test_get_roles_for_groups_on_project(self): + """Test retrieving group project roles. + + Test Plan: + + - Create two domains, two projects, six groups and six roles + - Project1 is in Domain1, Project2 is in Domain2 + - Domain2/Project2 are spoilers + - Assign a different direct group role to each project as well + as both an inherited and non-inherited role to each domain + - Get the group roles for Project 1 - depending on whether we have + enabled inheritance, we should either get back just the direct role + or both the direct one plus the inherited domain role from Domain 1 + + """ + domain1 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = unit.new_domain_ref() + self.resource_api.create_domain(domain2['id'], domain2) + project1 = unit.new_project_ref(domain_id=domain1['id']) + self.resource_api.create_project(project1['id'], project1) + project2 = unit.new_project_ref(domain_id=domain2['id']) + self.resource_api.create_project(project2['id'], project2) + group_list = [] + group_id_list = [] + role_list = [] + for _ in range(6): + group = unit.new_group_ref(domain_id=domain1['id']) + group = self.identity_api.create_group(group) + group_list.append(group) + group_id_list.append(group['id']) + + role = unit.new_role_ref() + self.role_api.create_role(role['id'], role) + role_list.append(role) + + # Assign the roles - one inherited and one non-inherited on Domain1, + # plus one on Project1 + self.assignment_api.create_grant(group_id=group_list[0]['id'], + domain_id=domain1['id'], + role_id=role_list[0]['id']) + self.assignment_api.create_grant(group_id=group_list[1]['id'], + domain_id=domain1['id'], + role_id=role_list[1]['id'], + inherited_to_projects=True) + self.assignment_api.create_grant(group_id=group_list[2]['id'], + project_id=project1['id'], + role_id=role_list[2]['id']) + + # ...and a duplicate set of spoiler assignments to Domain2/Project2 + self.assignment_api.create_grant(group_id=group_list[3]['id'], + domain_id=domain2['id'], + role_id=role_list[3]['id']) + self.assignment_api.create_grant(group_id=group_list[4]['id'], + domain_id=domain2['id'], + role_id=role_list[4]['id'], + inherited_to_projects=True) + self.assignment_api.create_grant(group_id=group_list[5]['id'], + project_id=project2['id'], + role_id=role_list[5]['id']) + + # Now get the effective roles for all groups on the Project1. With + # inheritance off, we should only get back the direct role. + + self.config_fixture.config(group='os_inherit', enabled=False) + role_refs = self.assignment_api.get_roles_for_groups( + group_id_list, project_id=project1['id']) + + self.assertThat(role_refs, matchers.HasLength(1)) + self.assertIn(role_list[2], role_refs) + + # With inheritance on, we should also get back the inherited role from + # its owning domain. + + self.config_fixture.config(group='os_inherit', enabled=True) + role_refs = self.assignment_api.get_roles_for_groups( + group_id_list, project_id=project1['id']) + + self.assertThat(role_refs, matchers.HasLength(2)) + self.assertIn(role_list[1], role_refs) + self.assertIn(role_list[2], role_refs) + + def test_list_domains_for_groups(self): + """Test retrieving domains for a list of groups. + + Test Plan: + + - Create three domains, three groups and one role + - Assign a non-inherited group role to two domains, and an inherited + group role to the third + - Ensure only the domains with non-inherited roles are returned + + """ + domain_list = [] + group_list = [] + group_id_list = [] + for _ in range(3): + domain = unit.new_domain_ref() + self.resource_api.create_domain(domain['id'], domain) + domain_list.append(domain) + + group = unit.new_group_ref(domain_id=domain['id']) + group = self.identity_api.create_group(group) + group_list.append(group) + group_id_list.append(group['id']) + + role1 = unit.new_role_ref() + self.role_api.create_role(role1['id'], role1) + + # Assign the roles - one is inherited + self.assignment_api.create_grant(group_id=group_list[0]['id'], + domain_id=domain_list[0]['id'], + role_id=role1['id']) + self.assignment_api.create_grant(group_id=group_list[1]['id'], + domain_id=domain_list[1]['id'], + role_id=role1['id']) + self.assignment_api.create_grant(group_id=group_list[2]['id'], + domain_id=domain_list[2]['id'], + role_id=role1['id'], + inherited_to_projects=True) + + # Now list the domains that have roles for any of the 3 groups + # We shouldn't get back domain[2] since that had an inherited role. + + domain_refs = ( + self.assignment_api.list_domains_for_groups(group_id_list)) + + self.assertThat(domain_refs, matchers.HasLength(2)) + self.assertIn(domain_list[0], domain_refs) + self.assertIn(domain_list[1], domain_refs) + + def test_list_projects_for_groups(self): + """Test retrieving projects for a list of groups. + + Test Plan: + + - Create two domains, four projects, seven groups and seven roles + - Project1-3 are in Domain1, Project4 is in Domain2 + - Domain2/Project4 are spoilers + - Project1 and 2 have direct group roles, Project3 has no direct + roles but should inherit a group role from Domain1 + - Get the projects for the group roles that are assigned to Project1 + Project2 and the inherited one on Domain1. Depending on whether we + have enabled inheritance, we should either get back just the projects + with direct roles (Project 1 and 2) or also Project3 due to its + inherited role from Domain1. + + """ + domain1 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = unit.new_domain_ref() + self.resource_api.create_domain(domain2['id'], domain2) + project1 = unit.new_project_ref(domain_id=domain1['id']) + project1 = self.resource_api.create_project(project1['id'], project1) + project2 = unit.new_project_ref(domain_id=domain1['id']) + project2 = self.resource_api.create_project(project2['id'], project2) + project3 = unit.new_project_ref(domain_id=domain1['id']) + project3 = self.resource_api.create_project(project3['id'], project3) + project4 = unit.new_project_ref(domain_id=domain2['id']) + project4 = self.resource_api.create_project(project4['id'], project4) + group_list = [] + role_list = [] + for _ in range(7): + group = unit.new_group_ref(domain_id=domain1['id']) + group = self.identity_api.create_group(group) + group_list.append(group) + + role = unit.new_role_ref() + self.role_api.create_role(role['id'], role) + role_list.append(role) + + # Assign the roles - one inherited and one non-inherited on Domain1, + # plus one on Project1 and Project2 + self.assignment_api.create_grant(group_id=group_list[0]['id'], + domain_id=domain1['id'], + role_id=role_list[0]['id']) + self.assignment_api.create_grant(group_id=group_list[1]['id'], + domain_id=domain1['id'], + role_id=role_list[1]['id'], + inherited_to_projects=True) + self.assignment_api.create_grant(group_id=group_list[2]['id'], + project_id=project1['id'], + role_id=role_list[2]['id']) + self.assignment_api.create_grant(group_id=group_list[3]['id'], + project_id=project2['id'], + role_id=role_list[3]['id']) + + # ...and a few of spoiler assignments to Domain2/Project4 + self.assignment_api.create_grant(group_id=group_list[4]['id'], + domain_id=domain2['id'], + role_id=role_list[4]['id']) + self.assignment_api.create_grant(group_id=group_list[5]['id'], + domain_id=domain2['id'], + role_id=role_list[5]['id'], + inherited_to_projects=True) + self.assignment_api.create_grant(group_id=group_list[6]['id'], + project_id=project4['id'], + role_id=role_list[6]['id']) + + # Now get the projects for the groups that have roles on Project1, + # Project2 and the inherited role on Domain!. With inheritance off, + # we should only get back the projects with direct role. + + self.config_fixture.config(group='os_inherit', enabled=False) + group_id_list = [group_list[1]['id'], group_list[2]['id'], + group_list[3]['id']] + project_refs = ( + self.assignment_api.list_projects_for_groups(group_id_list)) + + self.assertThat(project_refs, matchers.HasLength(2)) + self.assertIn(project1, project_refs) + self.assertIn(project2, project_refs) + + # With inheritance on, we should also get back the Project3 due to the + # inherited role from its owning domain. + + self.config_fixture.config(group='os_inherit', enabled=True) + project_refs = ( + self.assignment_api.list_projects_for_groups(group_id_list)) + + self.assertThat(project_refs, matchers.HasLength(3)) + self.assertIn(project1, project_refs) + self.assertIn(project2, project_refs) + self.assertIn(project3, project_refs) + + def test_update_role_no_name(self): + # A user can update a role and not include the name. + + # description is picked just because it's not name. + self.role_api.update_role(self.role_member['id'], + {'description': uuid.uuid4().hex}) + # If the previous line didn't raise an exception then the test passes. + + def test_update_role_same_name(self): + # A user can update a role and set the name to be the same as it was. + + self.role_api.update_role(self.role_member['id'], + {'name': self.role_member['name']}) + # If the previous line didn't raise an exception then the test passes. + + def test_list_role_assignment_containing_names(self): + # Create Refs + new_role = unit.new_role_ref() + new_domain = self._get_domain_fixture() + new_user = unit.new_user_ref(domain_id=new_domain['id']) + new_project = unit.new_project_ref(domain_id=new_domain['id']) + new_group = unit.new_group_ref(domain_id=new_domain['id']) + # Create entities + new_role = self.role_api.create_role(new_role['id'], new_role) + new_user = self.identity_api.create_user(new_user) + new_group = self.identity_api.create_group(new_group) + self.resource_api.create_project(new_project['id'], new_project) + self.assignment_api.create_grant(user_id=new_user['id'], + project_id=new_project['id'], + role_id=new_role['id']) + self.assignment_api.create_grant(group_id=new_group['id'], + project_id=new_project['id'], + role_id=new_role['id']) + self.assignment_api.create_grant(domain_id=new_domain['id'], + user_id=new_user['id'], + role_id=new_role['id']) + # Get the created assignments with the include_names flag + _asgmt_prj = self.assignment_api.list_role_assignments( + user_id=new_user['id'], + project_id=new_project['id'], + include_names=True) + _asgmt_grp = self.assignment_api.list_role_assignments( + group_id=new_group['id'], + project_id=new_project['id'], + include_names=True) + _asgmt_dmn = self.assignment_api.list_role_assignments( + domain_id=new_domain['id'], + user_id=new_user['id'], + include_names=True) + # Make sure we can get back the correct number of assignments + self.assertThat(_asgmt_prj, matchers.HasLength(1)) + self.assertThat(_asgmt_grp, matchers.HasLength(1)) + self.assertThat(_asgmt_dmn, matchers.HasLength(1)) + # get the first assignment + first_asgmt_prj = _asgmt_prj[0] + first_asgmt_grp = _asgmt_grp[0] + first_asgmt_dmn = _asgmt_dmn[0] + # Assert the names are correct in the project response + self.assertEqual(new_project['name'], + first_asgmt_prj['project_name']) + self.assertEqual(new_project['domain_id'], + first_asgmt_prj['project_domain_id']) + self.assertEqual(new_user['name'], + first_asgmt_prj['user_name']) + self.assertEqual(new_user['domain_id'], + first_asgmt_prj['user_domain_id']) + self.assertEqual(new_role['name'], + first_asgmt_prj['role_name']) + # Assert the names are correct in the group response + self.assertEqual(new_group['name'], + first_asgmt_grp['group_name']) + self.assertEqual(new_group['domain_id'], + first_asgmt_grp['group_domain_id']) + self.assertEqual(new_project['name'], + first_asgmt_grp['project_name']) + self.assertEqual(new_project['domain_id'], + first_asgmt_grp['project_domain_id']) + self.assertEqual(new_role['name'], + first_asgmt_grp['role_name']) + # Assert the names are correct in the domain response + self.assertEqual(new_domain['name'], + first_asgmt_dmn['domain_name']) + self.assertEqual(new_user['name'], + first_asgmt_dmn['user_name']) + self.assertEqual(new_user['domain_id'], + first_asgmt_dmn['user_domain_id']) + self.assertEqual(new_role['name'], + first_asgmt_dmn['role_name']) + + def test_list_role_assignment_does_not_contain_names(self): + """Test names are not included with list role assignments. + + Scenario: + - names are NOT included by default + - names are NOT included when include_names=False + + """ + def assert_does_not_contain_names(assignment): + first_asgmt_prj = assignment[0] + self.assertNotIn('project_name', first_asgmt_prj) + self.assertNotIn('project_domain_id', first_asgmt_prj) + self.assertNotIn('user_name', first_asgmt_prj) + self.assertNotIn('user_domain_id', first_asgmt_prj) + self.assertNotIn('role_name', first_asgmt_prj) + + # Create Refs + new_role = unit.new_role_ref() + new_domain = self._get_domain_fixture() + new_user = unit.new_user_ref(domain_id=new_domain['id']) + new_project = unit.new_project_ref(domain_id=new_domain['id']) + # Create entities + new_role = self.role_api.create_role(new_role['id'], new_role) + new_user = self.identity_api.create_user(new_user) + self.resource_api.create_project(new_project['id'], new_project) + self.assignment_api.create_grant(user_id=new_user['id'], + project_id=new_project['id'], + role_id=new_role['id']) + # Get the created assignments with NO include_names flag + role_assign_without_names = self.assignment_api.list_role_assignments( + user_id=new_user['id'], + project_id=new_project['id']) + assert_does_not_contain_names(role_assign_without_names) + # Get the created assignments with include_names=False + role_assign_without_names = self.assignment_api.list_role_assignments( + user_id=new_user['id'], + project_id=new_project['id'], + include_names=False) + assert_does_not_contain_names(role_assign_without_names) + + def test_delete_user_assignments_user_same_id_as_group(self): + """Test deleting user assignments when user_id == group_id. + + In this scenario, only user assignments must be deleted (i.e. + USER_DOMAIN or USER_PROJECT). + + Test plan: + * Create a user and a group with the same ID; + * Create four roles and assign them to both user and group; + * Delete all user assignments; + * Group assignments must stay intact. + """ + # Create a common ID + common_id = uuid.uuid4().hex + # Create a project + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + project = self.resource_api.create_project(project['id'], project) + # Create a user + user = unit.new_user_ref(id=common_id, + domain_id=CONF.identity.default_domain_id) + user = self.identity_api.driver.create_user(common_id, user) + self.assertEqual(common_id, user['id']) + # Create a group + group = unit.new_group_ref(id=common_id, + domain_id=CONF.identity.default_domain_id) + group = self.identity_api.driver.create_group(common_id, group) + self.assertEqual(common_id, group['id']) + # Create four roles + roles = [] + for _ in range(4): + role = unit.new_role_ref() + roles.append(self.role_api.create_role(role['id'], role)) + # Assign roles for user + self.assignment_api.driver.create_grant( + user_id=user['id'], domain_id=CONF.identity.default_domain_id, + role_id=roles[0]['id']) + self.assignment_api.driver.create_grant(user_id=user['id'], + project_id=project['id'], + role_id=roles[1]['id']) + # Assign roles for group + self.assignment_api.driver.create_grant( + group_id=group['id'], domain_id=CONF.identity.default_domain_id, + role_id=roles[2]['id']) + self.assignment_api.driver.create_grant(group_id=group['id'], + project_id=project['id'], + role_id=roles[3]['id']) + # Make sure they were assigned + user_assignments = self.assignment_api.list_role_assignments( + user_id=user['id']) + self.assertThat(user_assignments, matchers.HasLength(2)) + group_assignments = self.assignment_api.list_role_assignments( + group_id=group['id']) + self.assertThat(group_assignments, matchers.HasLength(2)) + # Delete user assignments + self.assignment_api.delete_user_assignments(user_id=user['id']) + # Assert only user assignments were deleted + user_assignments = self.assignment_api.list_role_assignments( + user_id=user['id']) + self.assertThat(user_assignments, matchers.HasLength(0)) + group_assignments = self.assignment_api.list_role_assignments( + group_id=group['id']) + self.assertThat(group_assignments, matchers.HasLength(2)) + # Make sure these remaining assignments are group-related + for assignment in group_assignments: + self.assertThat(assignment.keys(), matchers.Contains('group_id')) + + def test_delete_group_assignments_group_same_id_as_user(self): + """Test deleting group assignments when group_id == user_id. + + In this scenario, only group assignments must be deleted (i.e. + GROUP_DOMAIN or GROUP_PROJECT). + + Test plan: + * Create a group and a user with the same ID; + * Create four roles and assign them to both group and user; + * Delete all group assignments; + * User assignments must stay intact. + """ + # Create a common ID + common_id = uuid.uuid4().hex + # Create a project + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + project = self.resource_api.create_project(project['id'], project) + # Create a user + user = unit.new_user_ref(id=common_id, + domain_id=CONF.identity.default_domain_id) + user = self.identity_api.driver.create_user(common_id, user) + self.assertEqual(common_id, user['id']) + # Create a group + group = unit.new_group_ref(id=common_id, + domain_id=CONF.identity.default_domain_id) + group = self.identity_api.driver.create_group(common_id, group) + self.assertEqual(common_id, group['id']) + # Create four roles + roles = [] + for _ in range(4): + role = unit.new_role_ref() + roles.append(self.role_api.create_role(role['id'], role)) + # Assign roles for user + self.assignment_api.driver.create_grant( + user_id=user['id'], domain_id=CONF.identity.default_domain_id, + role_id=roles[0]['id']) + self.assignment_api.driver.create_grant(user_id=user['id'], + project_id=project['id'], + role_id=roles[1]['id']) + # Assign roles for group + self.assignment_api.driver.create_grant( + group_id=group['id'], domain_id=CONF.identity.default_domain_id, + role_id=roles[2]['id']) + self.assignment_api.driver.create_grant(group_id=group['id'], + project_id=project['id'], + role_id=roles[3]['id']) + # Make sure they were assigned + user_assignments = self.assignment_api.list_role_assignments( + user_id=user['id']) + self.assertThat(user_assignments, matchers.HasLength(2)) + group_assignments = self.assignment_api.list_role_assignments( + group_id=group['id']) + self.assertThat(group_assignments, matchers.HasLength(2)) + # Delete group assignments + self.assignment_api.delete_group_assignments(group_id=group['id']) + # Assert only group assignments were deleted + group_assignments = self.assignment_api.list_role_assignments( + group_id=group['id']) + self.assertThat(group_assignments, matchers.HasLength(0)) + user_assignments = self.assignment_api.list_role_assignments( + user_id=user['id']) + self.assertThat(user_assignments, matchers.HasLength(2)) + # Make sure these remaining assignments are user-related + for assignment in group_assignments: + self.assertThat(assignment.keys(), matchers.Contains('user_id')) + + def test_remove_foreign_assignments_when_deleting_a_domain(self): + # A user and a group are in default domain and have assigned a role on + # two new domains. This test makes sure that when one of the new + # domains is deleted, the role assignments for the user and the group + # from the default domain are deleted only on that domain. + group = unit.new_group_ref(domain_id=CONF.identity.default_domain_id) + group = self.identity_api.create_group(group) + + role = unit.new_role_ref() + role = self.role_api.create_role(role['id'], role) + + new_domains = [unit.new_domain_ref(), unit.new_domain_ref()] + for new_domain in new_domains: + self.resource_api.create_domain(new_domain['id'], new_domain) + + self.assignment_api.create_grant(group_id=group['id'], + domain_id=new_domain['id'], + role_id=role['id']) + self.assignment_api.create_grant(user_id=self.user_two['id'], + domain_id=new_domain['id'], + role_id=role['id']) + + # Check there are 4 role assignments for that role + role_assignments = self.assignment_api.list_role_assignments( + role_id=role['id']) + self.assertThat(role_assignments, matchers.HasLength(4)) + + # Delete first new domain and check only 2 assignments were left + self.resource_api.update_domain(new_domains[0]['id'], + {'enabled': False}) + self.resource_api.delete_domain(new_domains[0]['id']) + + role_assignments = self.assignment_api.list_role_assignments( + role_id=role['id']) + self.assertThat(role_assignments, matchers.HasLength(2)) + + # Delete second new domain and check no assignments were left + self.resource_api.update_domain(new_domains[1]['id'], + {'enabled': False}) + self.resource_api.delete_domain(new_domains[1]['id']) + + role_assignments = self.assignment_api.list_role_assignments( + role_id=role['id']) + self.assertEqual([], role_assignments) + + +class InheritanceTests(AssignmentTestHelperMixin): + + def test_role_assignments_user_domain_to_project_inheritance(self): + test_plan = { + 'entities': {'domains': {'users': 2, 'projects': 1}, + 'roles': 3}, + 'assignments': [{'user': 0, 'role': 0, 'domain': 0}, + {'user': 0, 'role': 1, 'project': 0}, + {'user': 0, 'role': 2, 'domain': 0, + 'inherited_to_projects': True}, + {'user': 1, 'role': 1, 'project': 0}], + 'tests': [ + # List all direct assignments for user[0] + {'params': {'user': 0}, + 'results': [{'user': 0, 'role': 0, 'domain': 0}, + {'user': 0, 'role': 1, 'project': 0}, + {'user': 0, 'role': 2, 'domain': 0, + 'inherited_to_projects': 'projects'}]}, + # Now the effective ones - so the domain role should turn into + # a project role + {'params': {'user': 0, 'effective': True}, + 'results': [{'user': 0, 'role': 0, 'domain': 0}, + {'user': 0, 'role': 1, 'project': 0}, + {'user': 0, 'role': 2, 'project': 0, + 'indirect': {'domain': 0}}]}, + # Narrow down to effective roles for user[0] and project[0] + {'params': {'user': 0, 'project': 0, 'effective': True}, + 'results': [{'user': 0, 'role': 1, 'project': 0}, + {'user': 0, 'role': 2, 'project': 0, + 'indirect': {'domain': 0}}]} + ] + } + self.config_fixture.config(group='os_inherit', enabled=True) + self.execute_assignment_plan(test_plan) + + def test_inherited_role_assignments_excluded_if_os_inherit_false(self): + test_plan = { + 'entities': {'domains': {'users': 2, 'groups': 1, 'projects': 1}, + 'roles': 4}, + 'group_memberships': [{'group': 0, 'users': [0]}], + 'assignments': [{'user': 0, 'role': 0, 'domain': 0}, + {'user': 0, 'role': 1, 'project': 0}, + {'user': 0, 'role': 2, 'domain': 0, + 'inherited_to_projects': True}, + {'user': 1, 'role': 1, 'project': 0}, + {'group': 0, 'role': 3, 'project': 0}], + 'tests': [ + # List all direct assignments for user[0], since os-inherit is + # disabled, we should not see the inherited role + {'params': {'user': 0}, + 'results': [{'user': 0, 'role': 0, 'domain': 0}, + {'user': 0, 'role': 1, 'project': 0}]}, + # Same in effective mode - inherited roles should not be + # included or expanded...but the group role should now + # turn up as a user role, since group expansion is not + # part of os-inherit. + {'params': {'user': 0, 'effective': True}, + 'results': [{'user': 0, 'role': 0, 'domain': 0}, + {'user': 0, 'role': 1, 'project': 0}, + {'user': 0, 'role': 3, 'project': 0, + 'indirect': {'group': 0}}]}, + ] + } + self.config_fixture.config(group='os_inherit', enabled=False) + self.execute_assignment_plan(test_plan) + + def _test_crud_inherited_and_direct_assignment(self, **kwargs): + """Tests inherited and direct assignments for the actor and target + + Ensure it is possible to create both inherited and direct role + assignments for the same actor on the same target. The actor and the + target are specified in the kwargs as ('user_id' or 'group_id') and + ('project_id' or 'domain_id'), respectively. + + """ + self.config_fixture.config(group='os_inherit', enabled=True) + # Create a new role to avoid assignments loaded from default fixtures + role = unit.new_role_ref() + role = self.role_api.create_role(role['id'], role) + + # Define the common assignment entity + assignment_entity = {'role_id': role['id']} + assignment_entity.update(kwargs) + + # Define assignments under test + direct_assignment_entity = assignment_entity.copy() + inherited_assignment_entity = assignment_entity.copy() + inherited_assignment_entity['inherited_to_projects'] = 'projects' + + # Create direct assignment and check grants + self.assignment_api.create_grant(inherited_to_projects=False, + **assignment_entity) + + grants = self.assignment_api.list_role_assignments(role_id=role['id']) + self.assertThat(grants, matchers.HasLength(1)) + self.assertIn(direct_assignment_entity, grants) + + # Now add inherited assignment and check grants + self.assignment_api.create_grant(inherited_to_projects=True, + **assignment_entity) + + grants = self.assignment_api.list_role_assignments(role_id=role['id']) + self.assertThat(grants, matchers.HasLength(2)) + self.assertIn(direct_assignment_entity, grants) + self.assertIn(inherited_assignment_entity, grants) + + # Delete both and check grants + self.assignment_api.delete_grant(inherited_to_projects=False, + **assignment_entity) + self.assignment_api.delete_grant(inherited_to_projects=True, + **assignment_entity) + + grants = self.assignment_api.list_role_assignments(role_id=role['id']) + self.assertEqual([], grants) + + def test_crud_inherited_and_direct_assignment_for_user_on_domain(self): + self._test_crud_inherited_and_direct_assignment( + user_id=self.user_foo['id'], + domain_id=CONF.identity.default_domain_id) + + def test_crud_inherited_and_direct_assignment_for_group_on_domain(self): + group = unit.new_group_ref(domain_id=CONF.identity.default_domain_id) + group = self.identity_api.create_group(group) + + self._test_crud_inherited_and_direct_assignment( + group_id=group['id'], domain_id=CONF.identity.default_domain_id) + + def test_crud_inherited_and_direct_assignment_for_user_on_project(self): + self._test_crud_inherited_and_direct_assignment( + user_id=self.user_foo['id'], project_id=self.tenant_baz['id']) + + def test_crud_inherited_and_direct_assignment_for_group_on_project(self): + group = unit.new_group_ref(domain_id=CONF.identity.default_domain_id) + group = self.identity_api.create_group(group) + + self._test_crud_inherited_and_direct_assignment( + group_id=group['id'], project_id=self.tenant_baz['id']) + + def test_inherited_role_grants_for_user(self): + """Test inherited user roles. + + Test Plan: + + - Enable OS-INHERIT extension + - Create 3 roles + - Create a domain, with a project and a user + - Check no roles yet exit + - Assign a direct user role to the project and a (non-inherited) + user role to the domain + - Get a list of effective roles - should only get the one direct role + - Now add an inherited user role to the domain + - Get a list of effective roles - should have two roles, one + direct and one by virtue of the inherited user role + - Also get effective roles for the domain - the role marked as + inherited should not show up + + """ + self.config_fixture.config(group='os_inherit', enabled=True) + role_list = [] + for _ in range(3): + role = unit.new_role_ref() + self.role_api.create_role(role['id'], role) + role_list.append(role) + domain1 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + user1 = unit.new_user_ref(domain_id=domain1['id']) + user1 = self.identity_api.create_user(user1) + project1 = unit.new_project_ref(domain_id=domain1['id']) + self.resource_api.create_project(project1['id'], project1) + + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + project_id=project1['id']) + self.assertEqual(0, len(roles_ref)) + + # Create the first two roles - the domain one is not inherited + self.assignment_api.create_grant(user_id=user1['id'], + project_id=project1['id'], + role_id=role_list[0]['id']) + self.assignment_api.create_grant(user_id=user1['id'], + domain_id=domain1['id'], + role_id=role_list[1]['id']) + + # Now get the effective roles for the user and project, this + # should only include the direct role assignment on the project + combined_list = self.assignment_api.get_roles_for_user_and_project( + user1['id'], project1['id']) + self.assertEqual(1, len(combined_list)) + self.assertIn(role_list[0]['id'], combined_list) + + # Now add an inherited role on the domain + self.assignment_api.create_grant(user_id=user1['id'], + domain_id=domain1['id'], + role_id=role_list[2]['id'], + inherited_to_projects=True) + + # Now get the effective roles for the user and project again, this + # should now include the inherited role on the domain + combined_list = self.assignment_api.get_roles_for_user_and_project( + user1['id'], project1['id']) + self.assertEqual(2, len(combined_list)) + self.assertIn(role_list[0]['id'], combined_list) + self.assertIn(role_list[2]['id'], combined_list) + + # Finally, check that the inherited role does not appear as a valid + # directly assigned role on the domain itself + combined_role_list = self.assignment_api.get_roles_for_user_and_domain( + user1['id'], domain1['id']) + self.assertEqual(1, len(combined_role_list)) + self.assertIn(role_list[1]['id'], combined_role_list) + + # TODO(henry-nash): The test above uses get_roles_for_user_and_project + # and get_roles_for_user_and_domain, which will, in a subsequent patch, + # be re-implemented to simply call list_role_assignments (see blueprint + # remove-role-metadata). + # + # The test plan below therefore mirrors this test, to ensure that + # list_role_assignments works the same. Once get_roles_for_user_and + # project/domain have been re-implemented then the manual tests above + # can be refactored to simply ensure it gives the same answers. + test_plan = { + # A domain with a user & project, plus 3 roles. + 'entities': {'domains': {'users': 1, 'projects': 1}, + 'roles': 3}, + 'assignments': [{'user': 0, 'role': 0, 'project': 0}, + {'user': 0, 'role': 1, 'domain': 0}, + {'user': 0, 'role': 2, 'domain': 0, + 'inherited_to_projects': True}], + 'tests': [ + # List all effective assignments for user[0] on project[0]. + # Should get one direct role and one inherited role. + {'params': {'user': 0, 'project': 0, 'effective': True}, + 'results': [{'user': 0, 'role': 0, 'project': 0}, + {'user': 0, 'role': 2, 'project': 0, + 'indirect': {'domain': 0}}]}, + # Ensure effective mode on the domain does not list the + # inherited role on that domain + {'params': {'user': 0, 'domain': 0, 'effective': True}, + 'results': [{'user': 0, 'role': 1, 'domain': 0}]}, + # Ensure non-inherited mode also only returns the non-inherited + # role on the domain + {'params': {'user': 0, 'domain': 0, 'inherited': False}, + 'results': [{'user': 0, 'role': 1, 'domain': 0}]}, + ] + } + self.execute_assignment_plan(test_plan) + + def test_inherited_role_grants_for_group(self): + """Test inherited group roles. + + Test Plan: + + - Enable OS-INHERIT extension + - Create 4 roles + - Create a domain, with a project, user and two groups + - Make the user a member of both groups + - Check no roles yet exit + - Assign a direct user role to the project and a (non-inherited) + group role on the domain + - Get a list of effective roles - should only get the one direct role + - Now add two inherited group roles to the domain + - Get a list of effective roles - should have three roles, one + direct and two by virtue of inherited group roles + + """ + self.config_fixture.config(group='os_inherit', enabled=True) + role_list = [] + for _ in range(4): + role = unit.new_role_ref() + self.role_api.create_role(role['id'], role) + role_list.append(role) + domain1 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + user1 = unit.new_user_ref(domain_id=domain1['id']) + user1 = self.identity_api.create_user(user1) + group1 = unit.new_group_ref(domain_id=domain1['id']) + group1 = self.identity_api.create_group(group1) + group2 = unit.new_group_ref(domain_id=domain1['id']) + group2 = self.identity_api.create_group(group2) + project1 = unit.new_project_ref(domain_id=domain1['id']) + self.resource_api.create_project(project1['id'], project1) + + self.identity_api.add_user_to_group(user1['id'], + group1['id']) + self.identity_api.add_user_to_group(user1['id'], + group2['id']) + + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + project_id=project1['id']) + self.assertEqual(0, len(roles_ref)) + + # Create two roles - the domain one is not inherited + self.assignment_api.create_grant(user_id=user1['id'], + project_id=project1['id'], + role_id=role_list[0]['id']) + self.assignment_api.create_grant(group_id=group1['id'], + domain_id=domain1['id'], + role_id=role_list[1]['id']) + + # Now get the effective roles for the user and project, this + # should only include the direct role assignment on the project + combined_list = self.assignment_api.get_roles_for_user_and_project( + user1['id'], project1['id']) + self.assertEqual(1, len(combined_list)) + self.assertIn(role_list[0]['id'], combined_list) + + # Now add to more group roles, both inherited, to the domain + self.assignment_api.create_grant(group_id=group2['id'], + domain_id=domain1['id'], + role_id=role_list[2]['id'], + inherited_to_projects=True) + self.assignment_api.create_grant(group_id=group2['id'], + domain_id=domain1['id'], + role_id=role_list[3]['id'], + inherited_to_projects=True) + + # Now get the effective roles for the user and project again, this + # should now include the inherited roles on the domain + combined_list = self.assignment_api.get_roles_for_user_and_project( + user1['id'], project1['id']) + self.assertEqual(3, len(combined_list)) + self.assertIn(role_list[0]['id'], combined_list) + self.assertIn(role_list[2]['id'], combined_list) + self.assertIn(role_list[3]['id'], combined_list) + + # TODO(henry-nash): The test above uses get_roles_for_user_and_project + # which will, in a subsequent patch, be re-implemented to simply call + # list_role_assignments (see blueprint remove-role-metadata). + # + # The test plan below therefore mirrors this test, to ensure that + # list_role_assignments works the same. Once + # get_roles_for_user_and_project has been re-implemented then the + # manual tests above can be refactored to simply ensure it gives + # the same answers. + test_plan = { + # A domain with a user and project, 2 groups, plus 4 roles. + 'entities': {'domains': {'users': 1, 'projects': 1, 'groups': 2}, + 'roles': 4}, + 'group_memberships': [{'group': 0, 'users': [0]}, + {'group': 1, 'users': [0]}], + 'assignments': [{'user': 0, 'role': 0, 'project': 0}, + {'group': 0, 'role': 1, 'domain': 0}, + {'group': 1, 'role': 2, 'domain': 0, + 'inherited_to_projects': True}, + {'group': 1, 'role': 3, 'domain': 0, + 'inherited_to_projects': True}], + 'tests': [ + # List all effective assignments for user[0] on project[0]. + # Should get one direct role and both inherited roles, but + # not the direct one on domain[0], even though user[0] is + # in group[0]. + {'params': {'user': 0, 'project': 0, 'effective': True}, + 'results': [{'user': 0, 'role': 0, 'project': 0}, + {'user': 0, 'role': 2, 'project': 0, + 'indirect': {'domain': 0, 'group': 1}}, + {'user': 0, 'role': 3, 'project': 0, + 'indirect': {'domain': 0, 'group': 1}}]} + ] + } + self.execute_assignment_plan(test_plan) + + def test_list_projects_for_user_with_inherited_grants(self): + """Test inherited user roles. + + Test Plan: + + - Enable OS-INHERIT extension + - Create a domain, with two projects and a user + - Assign an inherited user role on the domain, as well as a direct + user role to a separate project in a different domain + - Get a list of projects for user, should return all three projects + + """ + self.config_fixture.config(group='os_inherit', enabled=True) + domain = unit.new_domain_ref() + self.resource_api.create_domain(domain['id'], domain) + user1 = unit.new_user_ref(domain_id=domain['id']) + user1 = self.identity_api.create_user(user1) + project1 = unit.new_project_ref(domain_id=domain['id']) + self.resource_api.create_project(project1['id'], project1) + project2 = unit.new_project_ref(domain_id=domain['id']) + self.resource_api.create_project(project2['id'], project2) + + # Create 2 grants, one on a project and one inherited grant + # on the domain + self.assignment_api.create_grant(user_id=user1['id'], + project_id=self.tenant_bar['id'], + role_id=self.role_member['id']) + self.assignment_api.create_grant(user_id=user1['id'], + domain_id=domain['id'], + role_id=self.role_admin['id'], + inherited_to_projects=True) + # Should get back all three projects, one by virtue of the direct + # grant, plus both projects in the domain + user_projects = self.assignment_api.list_projects_for_user(user1['id']) + self.assertEqual(3, len(user_projects)) + + # TODO(henry-nash): The test above uses list_projects_for_user + # which may, in a subsequent patch, be re-implemented to call + # list_role_assignments and then report only the distinct projects. + # + # The test plan below therefore mirrors this test, to ensure that + # list_role_assignments works the same. Once list_projects_for_user + # has been re-implemented then the manual tests above can be + # refactored. + test_plan = { + # A domain with 1 project, plus a second domain with 2 projects, + # as well as a user. Also, create 2 roles. + 'entities': {'domains': [{'projects': 1}, + {'users': 1, 'projects': 2}], + 'roles': 2}, + 'assignments': [{'user': 0, 'role': 0, 'project': 0}, + {'user': 0, 'role': 1, 'domain': 1, + 'inherited_to_projects': True}], + 'tests': [ + # List all effective assignments for user[0] + # Should get one direct role plus one inherited role for each + # project in domain + {'params': {'user': 0, 'effective': True}, + 'results': [{'user': 0, 'role': 0, 'project': 0}, + {'user': 0, 'role': 1, 'project': 1, + 'indirect': {'domain': 1}}, + {'user': 0, 'role': 1, 'project': 2, + 'indirect': {'domain': 1}}]} + ] + } + self.execute_assignment_plan(test_plan) + + def test_list_projects_for_user_with_inherited_user_project_grants(self): + """Test inherited role assignments for users on nested projects. + + Test Plan: + + - Enable OS-INHERIT extension + - Create a hierarchy of projects with one root and one leaf project + - Assign an inherited user role on root project + - Assign a non-inherited user role on root project + - Get a list of projects for user, should return both projects + - Disable OS-INHERIT extension + - Get a list of projects for user, should return only root project + + """ + # Enable OS-INHERIT extension + self.config_fixture.config(group='os_inherit', enabled=True) + root_project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + root_project = self.resource_api.create_project(root_project['id'], + root_project) + leaf_project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id, + parent_id=root_project['id']) + leaf_project = self.resource_api.create_project(leaf_project['id'], + leaf_project) + + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user = self.identity_api.create_user(user) + + # Grant inherited user role + self.assignment_api.create_grant(user_id=user['id'], + project_id=root_project['id'], + role_id=self.role_admin['id'], + inherited_to_projects=True) + # Grant non-inherited user role + self.assignment_api.create_grant(user_id=user['id'], + project_id=root_project['id'], + role_id=self.role_member['id']) + # Should get back both projects: because the direct role assignment for + # the root project and inherited role assignment for leaf project + user_projects = self.assignment_api.list_projects_for_user(user['id']) + self.assertEqual(2, len(user_projects)) + self.assertIn(root_project, user_projects) + self.assertIn(leaf_project, user_projects) + + # Disable OS-INHERIT extension + self.config_fixture.config(group='os_inherit', enabled=False) + # Should get back just root project - due the direct role assignment + user_projects = self.assignment_api.list_projects_for_user(user['id']) + self.assertEqual(1, len(user_projects)) + self.assertIn(root_project, user_projects) + + # TODO(henry-nash): The test above uses list_projects_for_user + # which may, in a subsequent patch, be re-implemented to call + # list_role_assignments and then report only the distinct projects. + # + # The test plan below therefore mirrors this test, to ensure that + # list_role_assignments works the same. Once list_projects_for_user + # has been re-implemented then the manual tests above can be + # refactored. + test_plan = { + # A domain with a project and sub-project, plus a user. + # Also, create 2 roles. + 'entities': { + 'domains': {'id': CONF.identity.default_domain_id, 'users': 1, + 'projects': {'project': 1}}, + 'roles': 2}, + # A direct role and an inherited role on the parent + 'assignments': [{'user': 0, 'role': 0, 'project': 0}, + {'user': 0, 'role': 1, 'project': 0, + 'inherited_to_projects': True}], + 'tests': [ + # List all effective assignments for user[0] - should get back + # one direct role plus one inherited role. + {'params': {'user': 0, 'effective': True}, + 'results': [{'user': 0, 'role': 0, 'project': 0}, + {'user': 0, 'role': 1, 'project': 1, + 'indirect': {'project': 0}}]} + ] + } + + test_plan_with_os_inherit_disabled = { + 'tests': [ + # List all effective assignments for user[0] - should only get + # back the one direct role. + {'params': {'user': 0, 'effective': True}, + 'results': [{'user': 0, 'role': 0, 'project': 0}]} + ] + } + self.config_fixture.config(group='os_inherit', enabled=True) + test_data = self.execute_assignment_plan(test_plan) + self.config_fixture.config(group='os_inherit', enabled=False) + # Pass the existing test data in to allow execution of 2nd test plan + self.execute_assignment_cases( + test_plan_with_os_inherit_disabled, test_data) + + def test_list_projects_for_user_with_inherited_group_grants(self): + """Test inherited group roles. + + Test Plan: + + - Enable OS-INHERIT extension + - Create two domains, each with two projects + - Create a user and group + - Make the user a member of the group + - Assign a user role two projects, an inherited + group role to one domain and an inherited regular role on + the other domain + - Get a list of projects for user, should return both pairs of projects + from the domain, plus the one separate project + + """ + self.config_fixture.config(group='os_inherit', enabled=True) + domain = unit.new_domain_ref() + self.resource_api.create_domain(domain['id'], domain) + domain2 = unit.new_domain_ref() + self.resource_api.create_domain(domain2['id'], domain2) + project1 = unit.new_project_ref(domain_id=domain['id']) + self.resource_api.create_project(project1['id'], project1) + project2 = unit.new_project_ref(domain_id=domain['id']) + self.resource_api.create_project(project2['id'], project2) + project3 = unit.new_project_ref(domain_id=domain2['id']) + self.resource_api.create_project(project3['id'], project3) + project4 = unit.new_project_ref(domain_id=domain2['id']) + self.resource_api.create_project(project4['id'], project4) + user1 = unit.new_user_ref(domain_id=domain['id']) + user1 = self.identity_api.create_user(user1) + group1 = unit.new_group_ref(domain_id=domain['id']) + group1 = self.identity_api.create_group(group1) + self.identity_api.add_user_to_group(user1['id'], group1['id']) + + # Create 4 grants: + # - one user grant on a project in domain2 + # - one user grant on a project in the default domain + # - one inherited user grant on domain + # - one inherited group grant on domain2 + self.assignment_api.create_grant(user_id=user1['id'], + project_id=project3['id'], + role_id=self.role_member['id']) + self.assignment_api.create_grant(user_id=user1['id'], + project_id=self.tenant_bar['id'], + role_id=self.role_member['id']) + self.assignment_api.create_grant(user_id=user1['id'], + domain_id=domain['id'], + role_id=self.role_admin['id'], + inherited_to_projects=True) + self.assignment_api.create_grant(group_id=group1['id'], + domain_id=domain2['id'], + role_id=self.role_admin['id'], + inherited_to_projects=True) + # Should get back all five projects, but without a duplicate for + # project3 (since it has both a direct user role and an inherited role) + user_projects = self.assignment_api.list_projects_for_user(user1['id']) + self.assertEqual(5, len(user_projects)) + + # TODO(henry-nash): The test above uses list_projects_for_user + # which may, in a subsequent patch, be re-implemented to call + # list_role_assignments and then report only the distinct projects. + # + # The test plan below therefore mirrors this test, to ensure that + # list_role_assignments works the same. Once list_projects_for_user + # has been re-implemented then the manual tests above can be + # refactored. + test_plan = { + # A domain with a 1 project, plus a second domain with 2 projects, + # as well as a user & group and a 3rd domain with 2 projects. + # Also, created 2 roles. + 'entities': {'domains': [{'projects': 1}, + {'users': 1, 'groups': 1, 'projects': 2}, + {'projects': 2}], + 'roles': 2}, + 'group_memberships': [{'group': 0, 'users': [0]}], + 'assignments': [{'user': 0, 'role': 0, 'project': 0}, + {'user': 0, 'role': 0, 'project': 3}, + {'user': 0, 'role': 1, 'domain': 1, + 'inherited_to_projects': True}, + {'user': 0, 'role': 1, 'domain': 2, + 'inherited_to_projects': True}], + 'tests': [ + # List all effective assignments for user[0] + # Should get back both direct roles plus roles on both projects + # from each domain. Duplicates should not be filtered out. + {'params': {'user': 0, 'effective': True}, + 'results': [{'user': 0, 'role': 0, 'project': 3}, + {'user': 0, 'role': 0, 'project': 0}, + {'user': 0, 'role': 1, 'project': 1, + 'indirect': {'domain': 1}}, + {'user': 0, 'role': 1, 'project': 2, + 'indirect': {'domain': 1}}, + {'user': 0, 'role': 1, 'project': 3, + 'indirect': {'domain': 2}}, + {'user': 0, 'role': 1, 'project': 4, + 'indirect': {'domain': 2}}]} + ] + } + self.execute_assignment_plan(test_plan) + + def test_list_projects_for_user_with_inherited_group_project_grants(self): + """Test inherited role assignments for groups on nested projects. + + Test Plan: + + - Enable OS-INHERIT extension + - Create a hierarchy of projects with one root and one leaf project + - Assign an inherited group role on root project + - Assign a non-inherited group role on root project + - Get a list of projects for user, should return both projects + - Disable OS-INHERIT extension + - Get a list of projects for user, should return only root project + + """ + self.config_fixture.config(group='os_inherit', enabled=True) + root_project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + root_project = self.resource_api.create_project(root_project['id'], + root_project) + leaf_project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id, + parent_id=root_project['id']) + leaf_project = self.resource_api.create_project(leaf_project['id'], + leaf_project) + + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user = self.identity_api.create_user(user) + + group = unit.new_group_ref(domain_id=CONF.identity.default_domain_id) + group = self.identity_api.create_group(group) + self.identity_api.add_user_to_group(user['id'], group['id']) + + # Grant inherited group role + self.assignment_api.create_grant(group_id=group['id'], + project_id=root_project['id'], + role_id=self.role_admin['id'], + inherited_to_projects=True) + # Grant non-inherited group role + self.assignment_api.create_grant(group_id=group['id'], + project_id=root_project['id'], + role_id=self.role_member['id']) + # Should get back both projects: because the direct role assignment for + # the root project and inherited role assignment for leaf project + user_projects = self.assignment_api.list_projects_for_user(user['id']) + self.assertEqual(2, len(user_projects)) + self.assertIn(root_project, user_projects) + self.assertIn(leaf_project, user_projects) + + # Disable OS-INHERIT extension + self.config_fixture.config(group='os_inherit', enabled=False) + # Should get back just root project - due the direct role assignment + user_projects = self.assignment_api.list_projects_for_user(user['id']) + self.assertEqual(1, len(user_projects)) + self.assertIn(root_project, user_projects) + + # TODO(henry-nash): The test above uses list_projects_for_user + # which may, in a subsequent patch, be re-implemented to call + # list_role_assignments and then report only the distinct projects. + # + # The test plan below therefore mirrors this test, to ensure that + # list_role_assignments works the same. Once list_projects_for_user + # has been re-implemented then the manual tests above can be + # refactored. + test_plan = { + # A domain with a project ans sub-project, plus a user. + # Also, create 2 roles. + 'entities': { + 'domains': {'id': CONF.identity.default_domain_id, 'users': 1, + 'groups': 1, + 'projects': {'project': 1}}, + 'roles': 2}, + 'group_memberships': [{'group': 0, 'users': [0]}], + # A direct role and an inherited role on the parent + 'assignments': [{'group': 0, 'role': 0, 'project': 0}, + {'group': 0, 'role': 1, 'project': 0, + 'inherited_to_projects': True}], + 'tests': [ + # List all effective assignments for user[0] - should get back + # one direct role plus one inherited role. + {'params': {'user': 0, 'effective': True}, + 'results': [{'user': 0, 'role': 0, 'project': 0, + 'indirect': {'group': 0}}, + {'user': 0, 'role': 1, 'project': 1, + 'indirect': {'group': 0, 'project': 0}}]} + ] + } + + test_plan_with_os_inherit_disabled = { + 'tests': [ + # List all effective assignments for user[0] - should only get + # back the one direct role. + {'params': {'user': 0, 'effective': True}, + 'results': [{'user': 0, 'role': 0, 'project': 0, + 'indirect': {'group': 0}}]} + ] + } + self.config_fixture.config(group='os_inherit', enabled=True) + test_data = self.execute_assignment_plan(test_plan) + self.config_fixture.config(group='os_inherit', enabled=False) + # Pass the existing test data in to allow execution of 2nd test plan + self.execute_assignment_cases( + test_plan_with_os_inherit_disabled, test_data) + + def test_list_assignments_for_tree(self): + """Test we correctly list direct assignments for a tree""" + # Enable OS-INHERIT extension + self.config_fixture.config(group='os_inherit', enabled=True) + + test_plan = { + # Create a domain with a project hierarchy 3 levels deep: + # + # project 0 + # ____________|____________ + # | | + # project 1 project 4 + # ______|_____ ______|_____ + # | | | | + # project 2 project 3 project 5 project 6 + # + # Also, create 1 user and 4 roles. + 'entities': { + 'domains': { + 'projects': {'project': [{'project': 2}, + {'project': 2}]}, + 'users': 1}, + 'roles': 4}, + 'assignments': [ + # Direct assignment to projects 1 and 2 + {'user': 0, 'role': 0, 'project': 1}, + {'user': 0, 'role': 1, 'project': 2}, + # Also an inherited assignment on project 1 + {'user': 0, 'role': 2, 'project': 1, + 'inherited_to_projects': True}, + # ...and two spoiler assignments, one to the root and one + # to project 4 + {'user': 0, 'role': 0, 'project': 0}, + {'user': 0, 'role': 3, 'project': 4}], + 'tests': [ + # List all assignments for project 1 and its subtree. + {'params': {'project': 1, 'include_subtree': True}, + 'results': [ + # Only the actual assignments should be returned, no + # expansion of inherited assignments + {'user': 0, 'role': 0, 'project': 1}, + {'user': 0, 'role': 1, 'project': 2}, + {'user': 0, 'role': 2, 'project': 1, + 'inherited_to_projects': 'projects'}]} + ] + } + + self.execute_assignment_plan(test_plan) + + def test_list_effective_assignments_for_tree(self): + """Test we correctly list effective assignments for a tree""" + # Enable OS-INHERIT extension + self.config_fixture.config(group='os_inherit', enabled=True) + + test_plan = { + # Create a domain with a project hierarchy 3 levels deep: + # + # project 0 + # ____________|____________ + # | | + # project 1 project 4 + # ______|_____ ______|_____ + # | | | | + # project 2 project 3 project 5 project 6 + # + # Also, create 1 user and 4 roles. + 'entities': { + 'domains': { + 'projects': {'project': [{'project': 2}, + {'project': 2}]}, + 'users': 1}, + 'roles': 4}, + 'assignments': [ + # An inherited assignment on project 1 + {'user': 0, 'role': 1, 'project': 1, + 'inherited_to_projects': True}, + # A direct assignment to project 2 + {'user': 0, 'role': 2, 'project': 2}, + # ...and two spoiler assignments, one to the root and one + # to project 4 + {'user': 0, 'role': 0, 'project': 0}, + {'user': 0, 'role': 3, 'project': 4}], + 'tests': [ + # List all effective assignments for project 1 and its subtree. + {'params': {'project': 1, 'effective': True, + 'include_subtree': True}, + 'results': [ + # The inherited assignment on project 1 should appear only + # on its children + {'user': 0, 'role': 1, 'project': 2, + 'indirect': {'project': 1}}, + {'user': 0, 'role': 1, 'project': 3, + 'indirect': {'project': 1}}, + # And finally the direct assignment on project 2 + {'user': 0, 'role': 2, 'project': 2}]} + ] + } + + self.execute_assignment_plan(test_plan) + + def test_list_effective_assignments_for_tree_with_mixed_assignments(self): + """Test that we correctly combine assignments for a tree. + + In this test we want to ensure that when asking for a list of + assignments in a subtree, any assignments inherited from above the + subtree are correctly combined with any assignments within the subtree + itself. + + """ + # Enable OS-INHERIT extension + self.config_fixture.config(group='os_inherit', enabled=True) + + test_plan = { + # Create a domain with a project hierarchy 3 levels deep: + # + # project 0 + # ____________|____________ + # | | + # project 1 project 4 + # ______|_____ ______|_____ + # | | | | + # project 2 project 3 project 5 project 6 + # + # Also, create 2 users, 1 group and 4 roles. + 'entities': { + 'domains': { + 'projects': {'project': [{'project': 2}, + {'project': 2}]}, + 'users': 2, 'groups': 1}, + 'roles': 4}, + # Both users are part of the same group + 'group_memberships': [{'group': 0, 'users': [0, 1]}], + # We are going to ask for listing of assignment on project 1 and + # it's subtree. So first we'll add two inherited assignments above + # this (one user and one for a group that contains this user). + 'assignments': [{'user': 0, 'role': 0, 'project': 0, + 'inherited_to_projects': True}, + {'group': 0, 'role': 1, 'project': 0, + 'inherited_to_projects': True}, + # Now an inherited assignment on project 1 itself, + # which should ONLY show up on its children + {'user': 0, 'role': 2, 'project': 1, + 'inherited_to_projects': True}, + # ...and a direct assignment on one of those + # children + {'user': 0, 'role': 3, 'project': 2}, + # The rest are spoiler assignments + {'user': 0, 'role': 2, 'project': 5}, + {'user': 0, 'role': 3, 'project': 4}], + 'tests': [ + # List all effective assignments for project 1 and its subtree. + {'params': {'project': 1, 'user': 0, 'effective': True, + 'include_subtree': True}, + 'results': [ + # First, we should see the inherited user assignment from + # project 0 on all projects in the subtree + {'user': 0, 'role': 0, 'project': 1, + 'indirect': {'project': 0}}, + {'user': 0, 'role': 0, 'project': 2, + 'indirect': {'project': 0}}, + {'user': 0, 'role': 0, 'project': 3, + 'indirect': {'project': 0}}, + # Also the inherited group assignment from project 0 on + # the subtree + {'user': 0, 'role': 1, 'project': 1, + 'indirect': {'project': 0, 'group': 0}}, + {'user': 0, 'role': 1, 'project': 2, + 'indirect': {'project': 0, 'group': 0}}, + {'user': 0, 'role': 1, 'project': 3, + 'indirect': {'project': 0, 'group': 0}}, + # The inherited assignment on project 1 should appear only + # on its children + {'user': 0, 'role': 2, 'project': 2, + 'indirect': {'project': 1}}, + {'user': 0, 'role': 2, 'project': 3, + 'indirect': {'project': 1}}, + # And finally the direct assignment on project 2 + {'user': 0, 'role': 3, 'project': 2}]} + ] + } + + self.execute_assignment_plan(test_plan) + + def test_list_effective_assignments_for_tree_with_domain_assignments(self): + """Test we correctly honor domain inherited assignments on the tree""" + # Enable OS-INHERIT extension + self.config_fixture.config(group='os_inherit', enabled=True) + + test_plan = { + # Create a domain with a project hierarchy 3 levels deep: + # + # project 0 + # ____________|____________ + # | | + # project 1 project 4 + # ______|_____ ______|_____ + # | | | | + # project 2 project 3 project 5 project 6 + # + # Also, create 1 user and 4 roles. + 'entities': { + 'domains': { + 'projects': {'project': [{'project': 2}, + {'project': 2}]}, + 'users': 1}, + 'roles': 4}, + 'assignments': [ + # An inherited assignment on the domain (which should be + # applied to all the projects) + {'user': 0, 'role': 1, 'domain': 0, + 'inherited_to_projects': True}, + # A direct assignment to project 2 + {'user': 0, 'role': 2, 'project': 2}, + # ...and two spoiler assignments, one to the root and one + # to project 4 + {'user': 0, 'role': 0, 'project': 0}, + {'user': 0, 'role': 3, 'project': 4}], + 'tests': [ + # List all effective assignments for project 1 and its subtree. + {'params': {'project': 1, 'effective': True, + 'include_subtree': True}, + 'results': [ + # The inherited assignment from the domain should appear + # only on the part of the subtree we are interested in + {'user': 0, 'role': 1, 'project': 1, + 'indirect': {'domain': 0}}, + {'user': 0, 'role': 1, 'project': 2, + 'indirect': {'domain': 0}}, + {'user': 0, 'role': 1, 'project': 3, + 'indirect': {'domain': 0}}, + # And finally the direct assignment on project 2 + {'user': 0, 'role': 2, 'project': 2}]} + ] + } + + self.execute_assignment_plan(test_plan) + + def test_list_user_ids_for_project_with_inheritance(self): + test_plan = { + # A domain with a project and sub-project, plus four users, + # two groups, as well as 4 roles. + 'entities': { + 'domains': {'id': CONF.identity.default_domain_id, 'users': 4, + 'groups': 2, + 'projects': {'project': 1}}, + 'roles': 4}, + # Each group has a unique user member + 'group_memberships': [{'group': 0, 'users': [1]}, + {'group': 1, 'users': [3]}], + # Set up assignments so that there should end up with four + # effective assignments on project 1 - one direct, one due to + # group membership and one user assignment inherited from the + # parent and one group assignment inhertied from the parent. + 'assignments': [{'user': 0, 'role': 0, 'project': 1}, + {'group': 0, 'role': 1, 'project': 1}, + {'user': 2, 'role': 2, 'project': 0, + 'inherited_to_projects': True}, + {'group': 1, 'role': 3, 'project': 0, + 'inherited_to_projects': True}], + } + # Use assignment plan helper to create all the entities and + # assignments - then we'll run our own tests using the data + test_data = self.execute_assignment_plan(test_plan) + self.config_fixture.config(group='os_inherit', enabled=True) + user_ids = self.assignment_api.list_user_ids_for_project( + test_data['projects'][1]['id']) + self.assertThat(user_ids, matchers.HasLength(4)) + for x in range(0, 4): + self.assertIn(test_data['users'][x]['id'], user_ids) + + def test_list_role_assignment_using_inherited_sourced_groups(self): + """Test listing inherited assignments when restricted by groups.""" + test_plan = { + # A domain with 3 users, 3 groups, 3 projects, a second domain, + # plus 3 roles. + 'entities': {'domains': [{'users': 3, 'groups': 3, 'projects': 3}, + 1], + 'roles': 3}, + # Users 0 & 1 are in the group 0, User 0 also in group 1 + 'group_memberships': [{'group': 0, 'users': [0, 1]}, + {'group': 1, 'users': [0]}], + # Spread the assignments around - we want to be able to show that + # if sourced by group, assignments from other sources are excluded + 'assignments': [{'user': 0, 'role': 0, 'domain': 0}, + {'group': 0, 'role': 1, 'domain': 1}, + {'group': 1, 'role': 2, 'domain': 0, + 'inherited_to_projects': True}, + {'group': 1, 'role': 2, 'project': 1}, + {'user': 2, 'role': 1, 'project': 1, + 'inherited_to_projects': True}, + {'group': 2, 'role': 2, 'project': 2} + ], + 'tests': [ + # List all effective assignments sourced from groups 0 and 1. + # We should see the inherited group assigned on the 3 projects + # from domain 0, as well as the direct assignments. + {'params': {'source_from_group_ids': [0, 1], + 'effective': True}, + 'results': [{'group': 0, 'role': 1, 'domain': 1}, + {'group': 1, 'role': 2, 'project': 0, + 'indirect': {'domain': 0}}, + {'group': 1, 'role': 2, 'project': 1, + 'indirect': {'domain': 0}}, + {'group': 1, 'role': 2, 'project': 2, + 'indirect': {'domain': 0}}, + {'group': 1, 'role': 2, 'project': 1} + ]}, + ] + } + self.execute_assignment_plan(test_plan) + + +class ImpliedRoleTests(AssignmentTestHelperMixin): + + def test_implied_role_crd(self): + prior_role_ref = unit.new_role_ref() + self.role_api.create_role(prior_role_ref['id'], prior_role_ref) + implied_role_ref = unit.new_role_ref() + self.role_api.create_role(implied_role_ref['id'], implied_role_ref) + + self.role_api.create_implied_role( + prior_role_ref['id'], + implied_role_ref['id']) + implied_role = self.role_api.get_implied_role( + prior_role_ref['id'], + implied_role_ref['id']) + expected_implied_role_ref = { + 'prior_role_id': prior_role_ref['id'], + 'implied_role_id': implied_role_ref['id']} + self.assertDictContainsSubset( + expected_implied_role_ref, + implied_role) + + self.role_api.delete_implied_role( + prior_role_ref['id'], + implied_role_ref['id']) + self.assertRaises(exception.ImpliedRoleNotFound, + self.role_api.get_implied_role, + uuid.uuid4().hex, + uuid.uuid4().hex) + + def test_delete_implied_role_returns_not_found(self): + self.assertRaises(exception.ImpliedRoleNotFound, + self.role_api.delete_implied_role, + uuid.uuid4().hex, + uuid.uuid4().hex) + + def test_role_assignments_simple_tree_of_implied_roles(self): + """Test that implied roles are expanded out.""" + test_plan = { + 'entities': {'domains': {'users': 1, 'projects': 1}, + 'roles': 4}, + # Three level tree of implied roles + 'implied_roles': [{'role': 0, 'implied_roles': 1}, + {'role': 1, 'implied_roles': [2, 3]}], + 'assignments': [{'user': 0, 'role': 0, 'project': 0}], + 'tests': [ + # List all direct assignments for user[0], this should just + # show the one top level role assignment + {'params': {'user': 0}, + 'results': [{'user': 0, 'role': 0, 'project': 0}]}, + # Listing in effective mode should show the implied roles + # expanded out + {'params': {'user': 0, 'effective': True}, + 'results': [{'user': 0, 'role': 0, 'project': 0}, + {'user': 0, 'role': 1, 'project': 0, + 'indirect': {'role': 0}}, + {'user': 0, 'role': 2, 'project': 0, + 'indirect': {'role': 1}}, + {'user': 0, 'role': 3, 'project': 0, + 'indirect': {'role': 1}}]}, + ] + } + self.execute_assignment_plan(test_plan) + + def test_circular_inferences(self): + """Test that implied roles are expanded out.""" + test_plan = { + 'entities': {'domains': {'users': 1, 'projects': 1}, + 'roles': 4}, + # Three level tree of implied roles + 'implied_roles': [{'role': 0, 'implied_roles': [1]}, + {'role': 1, 'implied_roles': [2, 3]}, + {'role': 3, 'implied_roles': [0]}], + 'assignments': [{'user': 0, 'role': 0, 'project': 0}], + 'tests': [ + # List all direct assignments for user[0], this should just + # show the one top level role assignment + {'params': {'user': 0}, + 'results': [{'user': 0, 'role': 0, 'project': 0}]}, + # Listing in effective mode should show the implied roles + # expanded out + {'params': {'user': 0, 'effective': True}, + 'results': [{'user': 0, 'role': 0, 'project': 0}, + {'user': 0, 'role': 0, 'project': 0, + 'indirect': {'role': 3}}, + {'user': 0, 'role': 1, 'project': 0, + 'indirect': {'role': 0}}, + {'user': 0, 'role': 2, 'project': 0, + 'indirect': {'role': 1}}, + {'user': 0, 'role': 3, 'project': 0, + 'indirect': {'role': 1}}]}, + ] + } + self.execute_assignment_plan(test_plan) + + def test_role_assignments_directed_graph_of_implied_roles(self): + """Test that a role can have multiple, different prior roles.""" + test_plan = { + 'entities': {'domains': {'users': 1, 'projects': 1}, + 'roles': 6}, + # Three level tree of implied roles, where one of the roles at the + # bottom is implied by more than one top level role + 'implied_roles': [{'role': 0, 'implied_roles': [1, 2]}, + {'role': 1, 'implied_roles': [3, 4]}, + {'role': 5, 'implied_roles': 4}], + # The user gets both top level roles + 'assignments': [{'user': 0, 'role': 0, 'project': 0}, + {'user': 0, 'role': 5, 'project': 0}], + 'tests': [ + # The implied roles should be expanded out and there should be + # two entries for the role that had two different prior roles. + {'params': {'user': 0, 'effective': True}, + 'results': [{'user': 0, 'role': 0, 'project': 0}, + {'user': 0, 'role': 5, 'project': 0}, + {'user': 0, 'role': 1, 'project': 0, + 'indirect': {'role': 0}}, + {'user': 0, 'role': 2, 'project': 0, + 'indirect': {'role': 0}}, + {'user': 0, 'role': 3, 'project': 0, + 'indirect': {'role': 1}}, + {'user': 0, 'role': 4, 'project': 0, + 'indirect': {'role': 1}}, + {'user': 0, 'role': 4, 'project': 0, + 'indirect': {'role': 5}}]}, + ] + } + test_data = self.execute_assignment_plan(test_plan) + + # We should also be able to get a similar (yet summarized) answer to + # the above by calling get_roles_for_user_and_project(), which should + # list the role_ids, yet remove any duplicates + role_ids = self.assignment_api.get_roles_for_user_and_project( + test_data['users'][0]['id'], test_data['projects'][0]['id']) + # We should see 6 entries, not 7, since role index 5 appeared twice in + # the answer from list_role_assignments + self.assertThat(role_ids, matchers.HasLength(6)) + for x in range(0, 5): + self.assertIn(test_data['roles'][x]['id'], role_ids) + + def test_role_assignments_implied_roles_filtered_by_role(self): + """Test that you can filter by role even if roles are implied.""" + test_plan = { + 'entities': {'domains': {'users': 1, 'projects': 2}, + 'roles': 4}, + # Three level tree of implied roles + 'implied_roles': [{'role': 0, 'implied_roles': 1}, + {'role': 1, 'implied_roles': [2, 3]}], + 'assignments': [{'user': 0, 'role': 0, 'project': 0}, + {'user': 0, 'role': 3, 'project': 1}], + 'tests': [ + # List effective roles filtering by one of the implied roles, + # showing that the filter was implied post expansion of + # implied roles (and that non impled roles are included in + # the filter + {'params': {'role': 3, 'effective': True}, + 'results': [{'user': 0, 'role': 3, 'project': 0, + 'indirect': {'role': 1}}, + {'user': 0, 'role': 3, 'project': 1}]}, + ] + } + self.execute_assignment_plan(test_plan) + + def test_role_assignments_simple_tree_of_implied_roles_on_domain(self): + """Test that implied roles are expanded out when placed on a domain.""" + test_plan = { + 'entities': {'domains': {'users': 1}, + 'roles': 4}, + # Three level tree of implied roles + 'implied_roles': [{'role': 0, 'implied_roles': 1}, + {'role': 1, 'implied_roles': [2, 3]}], + 'assignments': [{'user': 0, 'role': 0, 'domain': 0}], + 'tests': [ + # List all direct assignments for user[0], this should just + # show the one top level role assignment + {'params': {'user': 0}, + 'results': [{'user': 0, 'role': 0, 'domain': 0}]}, + # Listing in effective mode should how the implied roles + # expanded out + {'params': {'user': 0, 'effective': True}, + 'results': [{'user': 0, 'role': 0, 'domain': 0}, + {'user': 0, 'role': 1, 'domain': 0, + 'indirect': {'role': 0}}, + {'user': 0, 'role': 2, 'domain': 0, + 'indirect': {'role': 1}}, + {'user': 0, 'role': 3, 'domain': 0, + 'indirect': {'role': 1}}]}, + ] + } + self.execute_assignment_plan(test_plan) + + def test_role_assignments_inherited_implied_roles(self): + """Test that you can intermix inherited and implied roles.""" + test_plan = { + 'entities': {'domains': {'users': 1, 'projects': 1}, + 'roles': 4}, + # Simply one level of implied roles + 'implied_roles': [{'role': 0, 'implied_roles': 1}], + # Assign to top level role as an inherited assignment to the + # domain + 'assignments': [{'user': 0, 'role': 0, 'domain': 0, + 'inherited_to_projects': True}], + 'tests': [ + # List all direct assignments for user[0], this should just + # show the one top level role assignment + {'params': {'user': 0}, + 'results': [{'user': 0, 'role': 0, 'domain': 0, + 'inherited_to_projects': 'projects'}]}, + # List in effective mode - we should only see the initial and + # implied role on the project (since inherited roles are not + # active on their anchor point). + {'params': {'user': 0, 'effective': True}, + 'results': [{'user': 0, 'role': 0, 'project': 0, + 'indirect': {'domain': 0}}, + {'user': 0, 'role': 1, 'project': 0, + 'indirect': {'domain': 0, 'role': 0}}]}, + ] + } + self.config_fixture.config(group='os_inherit', enabled=True) + self.execute_assignment_plan(test_plan) + + def test_role_assignments_domain_specific_with_implied_roles(self): + test_plan = { + 'entities': {'domains': {'users': 1, 'projects': 1, 'roles': 2}, + 'roles': 2}, + # Two level tree of implied roles, with the top and 1st level being + # domain specific roles, and the bottom level being infered global + # roles. + 'implied_roles': [{'role': 0, 'implied_roles': [1]}, + {'role': 1, 'implied_roles': [2, 3]}], + 'assignments': [{'user': 0, 'role': 0, 'project': 0}], + 'tests': [ + # List all direct assignments for user[0], this should just + # show the one top level role assignment, even though this is a + # domain specific role (since we are in non-effective mode and + # we show any direct role assignment in that mode). + {'params': {'user': 0}, + 'results': [{'user': 0, 'role': 0, 'project': 0}]}, + # Now the effective ones - so the implied roles should be + # expanded out, as well as any domain specific roles should be + # removed. + {'params': {'user': 0, 'effective': True}, + 'results': [{'user': 0, 'role': 2, 'project': 0, + 'indirect': {'role': 1}}, + {'user': 0, 'role': 3, 'project': 0, + 'indirect': {'role': 1}}]}, + ] + } + self.execute_assignment_plan(test_plan) diff --git a/keystone-moon/keystone/tests/unit/assignment/test_core.py b/keystone-moon/keystone/tests/unit/assignment/test_core.py new file mode 100644 index 00000000..494e19c3 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/assignment/test_core.py @@ -0,0 +1,123 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 copy +import uuid + +from keystone import exception +from keystone.tests import unit +from keystone.tests.unit import default_fixtures + + +class RoleTests(object): + + def test_get_role_returns_not_found(self): + self.assertRaises(exception.RoleNotFound, + self.role_api.get_role, + uuid.uuid4().hex) + + def test_create_duplicate_role_name_fails(self): + role = unit.new_role_ref(id='fake1', name='fake1name') + self.role_api.create_role('fake1', role) + role['id'] = 'fake2' + self.assertRaises(exception.Conflict, + self.role_api.create_role, + 'fake2', + role) + + def test_rename_duplicate_role_name_fails(self): + role1 = unit.new_role_ref(id='fake1', name='fake1name') + role2 = unit.new_role_ref(id='fake2', name='fake2name') + self.role_api.create_role('fake1', role1) + self.role_api.create_role('fake2', role2) + role1['name'] = 'fake2name' + self.assertRaises(exception.Conflict, + self.role_api.update_role, + 'fake1', + role1) + + def test_role_crud(self): + role = unit.new_role_ref() + self.role_api.create_role(role['id'], role) + role_ref = self.role_api.get_role(role['id']) + role_ref_dict = {x: role_ref[x] for x in role_ref} + self.assertDictEqual(role, role_ref_dict) + + role['name'] = uuid.uuid4().hex + updated_role_ref = self.role_api.update_role(role['id'], role) + role_ref = self.role_api.get_role(role['id']) + role_ref_dict = {x: role_ref[x] for x in role_ref} + self.assertDictEqual(role, role_ref_dict) + self.assertDictEqual(role_ref_dict, updated_role_ref) + + self.role_api.delete_role(role['id']) + self.assertRaises(exception.RoleNotFound, + self.role_api.get_role, + role['id']) + + def test_update_role_returns_not_found(self): + role = unit.new_role_ref() + self.assertRaises(exception.RoleNotFound, + self.role_api.update_role, + role['id'], + role) + + def test_list_roles(self): + roles = self.role_api.list_roles() + self.assertEqual(len(default_fixtures.ROLES), len(roles)) + role_ids = set(role['id'] for role in roles) + expected_role_ids = set(role['id'] for role in default_fixtures.ROLES) + self.assertEqual(expected_role_ids, role_ids) + + @unit.skip_if_cache_disabled('role') + def test_cache_layer_role_crud(self): + role = unit.new_role_ref() + role_id = role['id'] + # Create role + self.role_api.create_role(role_id, role) + role_ref = self.role_api.get_role(role_id) + updated_role_ref = copy.deepcopy(role_ref) + updated_role_ref['name'] = uuid.uuid4().hex + # Update role, bypassing the role api manager + self.role_api.driver.update_role(role_id, updated_role_ref) + # Verify get_role still returns old ref + self.assertDictEqual(role_ref, self.role_api.get_role(role_id)) + # Invalidate Cache + self.role_api.get_role.invalidate(self.role_api, role_id) + # Verify get_role returns the new role_ref + self.assertDictEqual(updated_role_ref, + self.role_api.get_role(role_id)) + # Update role back to original via the assignment api manager + self.role_api.update_role(role_id, role_ref) + # Verify get_role returns the original role ref + self.assertDictEqual(role_ref, self.role_api.get_role(role_id)) + # Delete role bypassing the role api manager + self.role_api.driver.delete_role(role_id) + # Verify get_role still returns the role_ref + self.assertDictEqual(role_ref, self.role_api.get_role(role_id)) + # Invalidate cache + self.role_api.get_role.invalidate(self.role_api, role_id) + # Verify RoleNotFound is now raised + self.assertRaises(exception.RoleNotFound, + self.role_api.get_role, + role_id) + # recreate role + self.role_api.create_role(role_id, role) + self.role_api.get_role(role_id) + # delete role via the assignment api manager + self.role_api.delete_role(role_id) + # verity RoleNotFound is now raised + self.assertRaises(exception.RoleNotFound, + self.role_api.get_role, + role_id) diff --git a/keystone-moon/keystone/tests/unit/backend/core_ldap.py b/keystone-moon/keystone/tests/unit/backend/core_ldap.py index 869bb620..8b72c62a 100644 --- a/keystone-moon/keystone/tests/unit/backend/core_ldap.py +++ b/keystone-moon/keystone/tests/unit/backend/core_ldap.py @@ -86,6 +86,7 @@ class BaseBackendLdapCommon(object): class BaseBackendLdap(object): """Mixin class to set up an all-LDAP configuration.""" + def setUp(self): # NOTE(dstanek): The database must be setup prior to calling the # parent's setUp. The parent's setUp uses services (like @@ -113,7 +114,7 @@ class BaseBackendLdapIdentitySqlEverythingElse(unit.SQLDriverOverrides): super(BaseBackendLdapIdentitySqlEverythingElse, self).setUp() self.clear_database() self.load_backends() - cache.configure_cache_region(cache.REGION) + cache.configure_cache() sqldb.recreate() self.load_fixtures(default_fixtures) @@ -137,6 +138,7 @@ class BaseBackendLdapIdentitySqlEverythingElseWithMapping(object): Setting backward_compatible_ids to False will enable this mapping. """ + def config_overrides(self): super(BaseBackendLdapIdentitySqlEverythingElseWithMapping, self).config_overrides() diff --git a/keystone-moon/keystone/tests/unit/backend/legacy_drivers/__init__.py b/keystone-moon/keystone/tests/unit/backend/legacy_drivers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/unit/backend/legacy_drivers/assignment/V8/__init__.py b/keystone-moon/keystone/tests/unit/backend/legacy_drivers/assignment/V8/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/unit/backend/legacy_drivers/assignment/V8/sql.py b/keystone-moon/keystone/tests/unit/backend/legacy_drivers/assignment/V8/sql.py new file mode 100644 index 00000000..da1490a7 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/backend/legacy_drivers/assignment/V8/sql.py @@ -0,0 +1,39 @@ +# 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.tests.unit import test_backend_sql + + +class SqlIdentityV8(test_backend_sql.SqlIdentity): + """Test that a V8 driver still passes the same tests. + + We use the SQL driver as an example of a V8 legacy driver. + + """ + + def config_overrides(self): + super(SqlIdentityV8, self).config_overrides() + # V8 SQL specific driver overrides + self.config_fixture.config( + group='assignment', + driver='keystone.assignment.V8_backends.sql.Assignment') + self.use_specific_sql_driver_version( + 'keystone.assignment', 'backends', 'V8_') + + def test_delete_project_assignments_same_id_as_domain(self): + self.skipTest("V8 doesn't support project acting as a domain.") + + def test_delete_user_assignments_user_same_id_as_group(self): + self.skipTest("Groups and users with the same ID are not supported.") + + def test_delete_group_assignments_group_same_id_as_user(self): + self.skipTest("Groups and users with the same ID are not supported.") diff --git a/keystone-moon/keystone/tests/unit/backend/legacy_drivers/assignment/__init__.py b/keystone-moon/keystone/tests/unit/backend/legacy_drivers/assignment/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/unit/backend/legacy_drivers/federation/V8/__init__.py b/keystone-moon/keystone/tests/unit/backend/legacy_drivers/federation/V8/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/unit/backend/legacy_drivers/federation/V8/api_v3.py b/keystone-moon/keystone/tests/unit/backend/legacy_drivers/federation/V8/api_v3.py new file mode 100644 index 00000000..d5469768 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/backend/legacy_drivers/federation/V8/api_v3.py @@ -0,0 +1,108 @@ +# 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 six.moves import http_client + +from keystone.tests.unit import test_v3_federation + + +class FederatedSetupMixinV8(object): + def useV8driver(self): + # We use the SQL driver as an example V8 driver, so override + # the current driver with that version. + self.config_fixture.config( + group='federation', + driver='keystone.federation.V8_backends.sql.Federation') + self.use_specific_sql_driver_version( + 'keystone.federation', 'backends', 'V8_') + + +class FederatedIdentityProviderTestsV8( + test_v3_federation.FederatedIdentityProviderTests, + FederatedSetupMixinV8): + """Test that a V8 driver still passes the same tests.""" + + def config_overrides(self): + super(FederatedIdentityProviderTestsV8, self).config_overrides() + self.useV8driver() + + def test_create_idp_remote_repeated(self): + """Creates two IdentityProvider entities with some remote_ids + + A remote_id is the same for both so the second IdP is not + created because of the uniqueness of the remote_ids + + Expect HTTP 409 Conflict code for the latter call. + + Note: V9 drivers and later augment the conflict message with + additional information, which won't be present if we are running + a V8 driver - so override the newer tests to just ensure a + conflict message is raised. + """ + body = self.default_body.copy() + repeated_remote_id = uuid.uuid4().hex + body['remote_ids'] = [uuid.uuid4().hex, + uuid.uuid4().hex, + uuid.uuid4().hex, + repeated_remote_id] + self._create_default_idp(body=body) + + url = self.base_url(suffix=uuid.uuid4().hex) + body['remote_ids'] = [uuid.uuid4().hex, + repeated_remote_id] + self.put(url, body={'identity_provider': body}, + expected_status=http_client.CONFLICT) + + def test_check_idp_uniqueness(self): + """Add same IdP twice. + + Expect HTTP 409 Conflict code for the latter call. + + Note: V9 drivers and later augment the conflict message with + additional information, which won't be present if we are running + a V8 driver - so override the newer tests to just ensure a + conflict message is raised. + """ + url = self.base_url(suffix=uuid.uuid4().hex) + body = self._http_idp_input() + self.put(url, body={'identity_provider': body}, + expected_status=http_client.CREATED) + self.put(url, body={'identity_provider': body}, + expected_status=http_client.CONFLICT) + + +class MappingCRUDTestsV8( + test_v3_federation.MappingCRUDTests, + FederatedSetupMixinV8): + """Test that a V8 driver still passes the same tests.""" + + def config_overrides(self): + super(MappingCRUDTestsV8, self).config_overrides() + self.useV8driver() + + +class ServiceProviderTestsV8( + test_v3_federation.ServiceProviderTests, + FederatedSetupMixinV8): + """Test that a V8 driver still passes the same tests.""" + + def config_overrides(self): + super(ServiceProviderTestsV8, self).config_overrides() + self.useV8driver() + + def test_filter_list_sp_by_id(self): + self.skipTest('Operation not supported in v8 and earlier drivers') + + def test_filter_list_sp_by_enabled(self): + self.skipTest('Operation not supported in v8 and earlier drivers') diff --git a/keystone-moon/keystone/tests/unit/backend/legacy_drivers/federation/__init__.py b/keystone-moon/keystone/tests/unit/backend/legacy_drivers/federation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/unit/backend/legacy_drivers/resource/V8/__init__.py b/keystone-moon/keystone/tests/unit/backend/legacy_drivers/resource/V8/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/unit/backend/legacy_drivers/resource/V8/sql.py b/keystone-moon/keystone/tests/unit/backend/legacy_drivers/resource/V8/sql.py new file mode 100644 index 00000000..16acbdc3 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/backend/legacy_drivers/resource/V8/sql.py @@ -0,0 +1,71 @@ +# 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 unittest + +from keystone.resource.V8_backends import sql +from keystone.tests import unit +from keystone.tests.unit.ksfixtures import database +from keystone.tests.unit.resource import test_backends +from keystone.tests.unit import test_backend_sql + + +class SqlIdentityV8(test_backend_sql.SqlIdentity): + """Test that a V8 driver still passes the same tests. + + We use the SQL driver as an example of a V8 legacy driver. + + """ + + def config_overrides(self): + super(SqlIdentityV8, self).config_overrides() + # V8 SQL specific driver overrides + self.config_fixture.config( + group='resource', + driver='keystone.resource.V8_backends.sql.Resource') + self.use_specific_sql_driver_version( + 'keystone.resource', 'backends', 'V8_') + + def test_delete_projects_from_ids(self): + self.skipTest('Operation not supported in v8 and earlier drivers') + + def test_delete_projects_from_ids_with_no_existing_project_id(self): + self.skipTest('Operation not supported in v8 and earlier drivers') + + def test_delete_project_cascade(self): + self.skipTest('Operation not supported in v8 and earlier drivers') + + def test_delete_large_project_cascade(self): + self.skipTest('Operation not supported in v8 and earlier drivers') + + def test_hidden_project_domain_root_is_really_hidden(self): + self.skipTest('Operation not supported in v8 and earlier drivers') + + +class TestSqlResourceDriverV8(unit.BaseTestCase, + test_backends.ResourceDriverTests): + def setUp(self): + super(TestSqlResourceDriverV8, self).setUp() + + version_specifiers = { + 'keystone.resource': { + 'versionless_backend': 'backends', + 'versioned_backend': 'V8_backends' + } + } + self.useFixture(database.Database(version_specifiers)) + + self.driver = sql.Resource() + + @unittest.skip('Null domain not allowed.') + def test_create_project_null_domain(self): + pass diff --git a/keystone-moon/keystone/tests/unit/backend/legacy_drivers/resource/__init__.py b/keystone-moon/keystone/tests/unit/backend/legacy_drivers/resource/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/unit/backend/legacy_drivers/role/V8/__init__.py b/keystone-moon/keystone/tests/unit/backend/legacy_drivers/role/V8/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/unit/backend/legacy_drivers/role/V8/sql.py b/keystone-moon/keystone/tests/unit/backend/legacy_drivers/role/V8/sql.py new file mode 100644 index 00000000..d9378c30 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/backend/legacy_drivers/role/V8/sql.py @@ -0,0 +1,30 @@ +# 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.tests.unit import test_backend_sql + + +class SqlIdentityV8(test_backend_sql.SqlIdentity): + """Test that a V8 driver still passes the same tests. + + We use the SQL driver as an example of a V8 legacy driver. + + """ + + def config_overrides(self): + super(SqlIdentityV8, self).config_overrides() + # V8 SQL specific driver overrides + self.config_fixture.config( + group='role', + driver='keystone.assignment.V8_role_backends.sql.Role') + self.use_specific_sql_driver_version( + 'keystone.assignment', 'role_backends', 'V8_') diff --git a/keystone-moon/keystone/tests/unit/backend/legacy_drivers/role/__init__.py b/keystone-moon/keystone/tests/unit/backend/legacy_drivers/role/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/unit/catalog/test_backends.py b/keystone-moon/keystone/tests/unit/catalog/test_backends.py new file mode 100644 index 00000000..55898015 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/catalog/test_backends.py @@ -0,0 +1,588 @@ +# 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 copy +import uuid + +import mock +from six.moves import range +from testtools import matchers + +from keystone.catalog import core +from keystone.common import driver_hints +from keystone import exception +from keystone.tests import unit + + +class CatalogTests(object): + + _legacy_endpoint_id_in_endpoint = True + _enabled_default_to_true_when_creating_endpoint = False + + def test_region_crud(self): + # create + region_id = '0' * 255 + new_region = unit.new_region_ref(id=region_id) + res = self.catalog_api.create_region(new_region) + + # Ensure that we don't need to have a + # parent_region_id in the original supplied + # ref dict, but that it will be returned from + # the endpoint, with None value. + expected_region = new_region.copy() + expected_region['parent_region_id'] = None + self.assertDictEqual(expected_region, res) + + # Test adding another region with the one above + # as its parent. We will check below whether deleting + # the parent successfully deletes any child regions. + parent_region_id = region_id + new_region = unit.new_region_ref(parent_region_id=parent_region_id) + region_id = new_region['id'] + res = self.catalog_api.create_region(new_region) + self.assertDictEqual(new_region, res) + + # list + regions = self.catalog_api.list_regions() + self.assertThat(regions, matchers.HasLength(2)) + region_ids = [x['id'] for x in regions] + self.assertIn(parent_region_id, region_ids) + self.assertIn(region_id, region_ids) + + # update + region_desc_update = {'description': uuid.uuid4().hex} + res = self.catalog_api.update_region(region_id, region_desc_update) + expected_region = new_region.copy() + expected_region['description'] = region_desc_update['description'] + self.assertDictEqual(expected_region, res) + + # delete + self.catalog_api.delete_region(parent_region_id) + self.assertRaises(exception.RegionNotFound, + self.catalog_api.delete_region, + parent_region_id) + self.assertRaises(exception.RegionNotFound, + self.catalog_api.get_region, + parent_region_id) + # Ensure the child is also gone... + self.assertRaises(exception.RegionNotFound, + self.catalog_api.get_region, + region_id) + + def _create_region_with_parent_id(self, parent_id=None): + new_region = unit.new_region_ref(parent_region_id=parent_id) + self.catalog_api.create_region(new_region) + return new_region + + def test_list_regions_filtered_by_parent_region_id(self): + new_region = self._create_region_with_parent_id() + parent_id = new_region['id'] + new_region = self._create_region_with_parent_id(parent_id) + new_region = self._create_region_with_parent_id(parent_id) + + # filter by parent_region_id + hints = driver_hints.Hints() + hints.add_filter('parent_region_id', parent_id) + regions = self.catalog_api.list_regions(hints) + for region in regions: + self.assertEqual(parent_id, region['parent_region_id']) + + @unit.skip_if_cache_disabled('catalog') + def test_cache_layer_region_crud(self): + new_region = unit.new_region_ref() + region_id = new_region['id'] + self.catalog_api.create_region(new_region.copy()) + updated_region = copy.deepcopy(new_region) + updated_region['description'] = uuid.uuid4().hex + # cache the result + self.catalog_api.get_region(region_id) + # update the region bypassing catalog_api + self.catalog_api.driver.update_region(region_id, updated_region) + self.assertDictContainsSubset(new_region, + self.catalog_api.get_region(region_id)) + self.catalog_api.get_region.invalidate(self.catalog_api, region_id) + self.assertDictContainsSubset(updated_region, + self.catalog_api.get_region(region_id)) + # delete the region + self.catalog_api.driver.delete_region(region_id) + # still get the old region + self.assertDictContainsSubset(updated_region, + self.catalog_api.get_region(region_id)) + self.catalog_api.get_region.invalidate(self.catalog_api, region_id) + self.assertRaises(exception.RegionNotFound, + self.catalog_api.get_region, region_id) + + @unit.skip_if_cache_disabled('catalog') + def test_invalidate_cache_when_updating_region(self): + new_region = unit.new_region_ref() + region_id = new_region['id'] + self.catalog_api.create_region(new_region) + + # cache the region + self.catalog_api.get_region(region_id) + + # update the region via catalog_api + new_description = {'description': uuid.uuid4().hex} + self.catalog_api.update_region(region_id, new_description) + + # assert that we can get the new region + current_region = self.catalog_api.get_region(region_id) + self.assertEqual(new_description['description'], + current_region['description']) + + def test_create_region_with_duplicate_id(self): + new_region = unit.new_region_ref() + self.catalog_api.create_region(new_region) + # Create region again with duplicate id + self.assertRaises(exception.Conflict, + self.catalog_api.create_region, + new_region) + + def test_get_region_returns_not_found(self): + self.assertRaises(exception.RegionNotFound, + self.catalog_api.get_region, + uuid.uuid4().hex) + + def test_delete_region_returns_not_found(self): + self.assertRaises(exception.RegionNotFound, + self.catalog_api.delete_region, + uuid.uuid4().hex) + + def test_create_region_invalid_parent_region_returns_not_found(self): + new_region = unit.new_region_ref(parent_region_id='nonexisting') + self.assertRaises(exception.RegionNotFound, + self.catalog_api.create_region, + new_region) + + def test_avoid_creating_circular_references_in_regions_update(self): + region_one = self._create_region_with_parent_id() + + # self circle: region_one->region_one + self.assertRaises(exception.CircularRegionHierarchyError, + self.catalog_api.update_region, + region_one['id'], + {'parent_region_id': region_one['id']}) + + # region_one->region_two->region_one + region_two = self._create_region_with_parent_id(region_one['id']) + self.assertRaises(exception.CircularRegionHierarchyError, + self.catalog_api.update_region, + region_one['id'], + {'parent_region_id': region_two['id']}) + + # region_one region_two->region_three->region_four->region_two + region_three = self._create_region_with_parent_id(region_two['id']) + region_four = self._create_region_with_parent_id(region_three['id']) + self.assertRaises(exception.CircularRegionHierarchyError, + self.catalog_api.update_region, + region_two['id'], + {'parent_region_id': region_four['id']}) + + @mock.patch.object(core.CatalogDriverV8, + "_ensure_no_circle_in_hierarchical_regions") + def test_circular_regions_can_be_deleted(self, mock_ensure_on_circle): + # turn off the enforcement so that cycles can be created for the test + mock_ensure_on_circle.return_value = None + + region_one = self._create_region_with_parent_id() + + # self circle: region_one->region_one + self.catalog_api.update_region( + region_one['id'], + {'parent_region_id': region_one['id']}) + self.catalog_api.delete_region(region_one['id']) + self.assertRaises(exception.RegionNotFound, + self.catalog_api.get_region, + region_one['id']) + + # region_one->region_two->region_one + region_one = self._create_region_with_parent_id() + region_two = self._create_region_with_parent_id(region_one['id']) + self.catalog_api.update_region( + region_one['id'], + {'parent_region_id': region_two['id']}) + self.catalog_api.delete_region(region_one['id']) + self.assertRaises(exception.RegionNotFound, + self.catalog_api.get_region, + region_one['id']) + self.assertRaises(exception.RegionNotFound, + self.catalog_api.get_region, + region_two['id']) + + # region_one->region_two->region_three->region_one + region_one = self._create_region_with_parent_id() + region_two = self._create_region_with_parent_id(region_one['id']) + region_three = self._create_region_with_parent_id(region_two['id']) + self.catalog_api.update_region( + region_one['id'], + {'parent_region_id': region_three['id']}) + self.catalog_api.delete_region(region_two['id']) + self.assertRaises(exception.RegionNotFound, + self.catalog_api.get_region, + region_two['id']) + self.assertRaises(exception.RegionNotFound, + self.catalog_api.get_region, + region_one['id']) + self.assertRaises(exception.RegionNotFound, + self.catalog_api.get_region, + region_three['id']) + + def test_service_crud(self): + # create + new_service = unit.new_service_ref() + service_id = new_service['id'] + res = self.catalog_api.create_service(service_id, new_service) + self.assertDictEqual(new_service, res) + + # list + services = self.catalog_api.list_services() + self.assertIn(service_id, [x['id'] for x in services]) + + # update + service_name_update = {'name': uuid.uuid4().hex} + res = self.catalog_api.update_service(service_id, service_name_update) + expected_service = new_service.copy() + expected_service['name'] = service_name_update['name'] + self.assertDictEqual(expected_service, res) + + # delete + self.catalog_api.delete_service(service_id) + self.assertRaises(exception.ServiceNotFound, + self.catalog_api.delete_service, + service_id) + self.assertRaises(exception.ServiceNotFound, + self.catalog_api.get_service, + service_id) + + def _create_random_service(self): + new_service = unit.new_service_ref() + service_id = new_service['id'] + return self.catalog_api.create_service(service_id, new_service) + + def test_service_filtering(self): + target_service = self._create_random_service() + unrelated_service1 = self._create_random_service() + unrelated_service2 = self._create_random_service() + + # filter by type + hint_for_type = driver_hints.Hints() + hint_for_type.add_filter(name="type", value=target_service['type']) + services = self.catalog_api.list_services(hint_for_type) + + self.assertEqual(1, len(services)) + filtered_service = services[0] + self.assertEqual(target_service['type'], filtered_service['type']) + self.assertEqual(target_service['id'], filtered_service['id']) + + # filter should have been removed, since it was already used by the + # backend + self.assertEqual(0, len(hint_for_type.filters)) + + # the backend shouldn't filter by name, since this is handled by the + # front end + hint_for_name = driver_hints.Hints() + hint_for_name.add_filter(name="name", value=target_service['name']) + services = self.catalog_api.list_services(hint_for_name) + + self.assertEqual(3, len(services)) + + # filter should still be there, since it wasn't used by the backend + self.assertEqual(1, len(hint_for_name.filters)) + + self.catalog_api.delete_service(target_service['id']) + self.catalog_api.delete_service(unrelated_service1['id']) + self.catalog_api.delete_service(unrelated_service2['id']) + + @unit.skip_if_cache_disabled('catalog') + def test_cache_layer_service_crud(self): + new_service = unit.new_service_ref() + service_id = new_service['id'] + res = self.catalog_api.create_service(service_id, new_service) + self.assertDictEqual(new_service, res) + self.catalog_api.get_service(service_id) + updated_service = copy.deepcopy(new_service) + updated_service['description'] = uuid.uuid4().hex + # update bypassing catalog api + self.catalog_api.driver.update_service(service_id, updated_service) + self.assertDictContainsSubset(new_service, + self.catalog_api.get_service(service_id)) + self.catalog_api.get_service.invalidate(self.catalog_api, service_id) + self.assertDictContainsSubset(updated_service, + self.catalog_api.get_service(service_id)) + + # delete bypassing catalog api + self.catalog_api.driver.delete_service(service_id) + self.assertDictContainsSubset(updated_service, + self.catalog_api.get_service(service_id)) + self.catalog_api.get_service.invalidate(self.catalog_api, service_id) + self.assertRaises(exception.ServiceNotFound, + self.catalog_api.delete_service, + service_id) + self.assertRaises(exception.ServiceNotFound, + self.catalog_api.get_service, + service_id) + + @unit.skip_if_cache_disabled('catalog') + def test_invalidate_cache_when_updating_service(self): + new_service = unit.new_service_ref() + service_id = new_service['id'] + self.catalog_api.create_service(service_id, new_service) + + # cache the service + self.catalog_api.get_service(service_id) + + # update the service via catalog api + new_type = {'type': uuid.uuid4().hex} + self.catalog_api.update_service(service_id, new_type) + + # assert that we can get the new service + current_service = self.catalog_api.get_service(service_id) + self.assertEqual(new_type['type'], current_service['type']) + + def test_delete_service_with_endpoint(self): + # create a service + service = unit.new_service_ref() + self.catalog_api.create_service(service['id'], service) + + # create an endpoint attached to the service + endpoint = unit.new_endpoint_ref(service_id=service['id'], + region_id=None) + self.catalog_api.create_endpoint(endpoint['id'], endpoint) + + # deleting the service should also delete the endpoint + self.catalog_api.delete_service(service['id']) + self.assertRaises(exception.EndpointNotFound, + self.catalog_api.get_endpoint, + endpoint['id']) + self.assertRaises(exception.EndpointNotFound, + self.catalog_api.delete_endpoint, + endpoint['id']) + + def test_cache_layer_delete_service_with_endpoint(self): + service = unit.new_service_ref() + self.catalog_api.create_service(service['id'], service) + + # create an endpoint attached to the service + endpoint = unit.new_endpoint_ref(service_id=service['id'], + region_id=None) + self.catalog_api.create_endpoint(endpoint['id'], endpoint) + # cache the result + self.catalog_api.get_service(service['id']) + self.catalog_api.get_endpoint(endpoint['id']) + # delete the service bypassing catalog api + self.catalog_api.driver.delete_service(service['id']) + self.assertDictContainsSubset(endpoint, + self.catalog_api. + get_endpoint(endpoint['id'])) + self.assertDictContainsSubset(service, + self.catalog_api. + get_service(service['id'])) + self.catalog_api.get_endpoint.invalidate(self.catalog_api, + endpoint['id']) + self.assertRaises(exception.EndpointNotFound, + self.catalog_api.get_endpoint, + endpoint['id']) + self.assertRaises(exception.EndpointNotFound, + self.catalog_api.delete_endpoint, + endpoint['id']) + # multiple endpoints associated with a service + second_endpoint = unit.new_endpoint_ref(service_id=service['id'], + region_id=None) + self.catalog_api.create_service(service['id'], service) + self.catalog_api.create_endpoint(endpoint['id'], endpoint) + self.catalog_api.create_endpoint(second_endpoint['id'], + second_endpoint) + self.catalog_api.delete_service(service['id']) + self.assertRaises(exception.EndpointNotFound, + self.catalog_api.get_endpoint, + endpoint['id']) + self.assertRaises(exception.EndpointNotFound, + self.catalog_api.delete_endpoint, + endpoint['id']) + self.assertRaises(exception.EndpointNotFound, + self.catalog_api.get_endpoint, + second_endpoint['id']) + self.assertRaises(exception.EndpointNotFound, + self.catalog_api.delete_endpoint, + second_endpoint['id']) + + def test_get_service_returns_not_found(self): + self.assertRaises(exception.ServiceNotFound, + self.catalog_api.get_service, + uuid.uuid4().hex) + + def test_delete_service_returns_not_found(self): + self.assertRaises(exception.ServiceNotFound, + self.catalog_api.delete_service, + uuid.uuid4().hex) + + def test_create_endpoint_nonexistent_service(self): + endpoint = unit.new_endpoint_ref(service_id=uuid.uuid4().hex, + region_id=None) + self.assertRaises(exception.ValidationError, + self.catalog_api.create_endpoint, + endpoint['id'], + endpoint) + + def test_update_endpoint_nonexistent_service(self): + dummy_service, enabled_endpoint, dummy_disabled_endpoint = ( + self._create_endpoints()) + new_endpoint = unit.new_endpoint_ref(service_id=uuid.uuid4().hex) + self.assertRaises(exception.ValidationError, + self.catalog_api.update_endpoint, + enabled_endpoint['id'], + new_endpoint) + + def test_create_endpoint_nonexistent_region(self): + service = unit.new_service_ref() + self.catalog_api.create_service(service['id'], service) + + endpoint = unit.new_endpoint_ref(service_id=service['id']) + self.assertRaises(exception.ValidationError, + self.catalog_api.create_endpoint, + endpoint['id'], + endpoint) + + def test_update_endpoint_nonexistent_region(self): + dummy_service, enabled_endpoint, dummy_disabled_endpoint = ( + self._create_endpoints()) + new_endpoint = unit.new_endpoint_ref(service_id=uuid.uuid4().hex) + self.assertRaises(exception.ValidationError, + self.catalog_api.update_endpoint, + enabled_endpoint['id'], + new_endpoint) + + def test_get_endpoint_returns_not_found(self): + self.assertRaises(exception.EndpointNotFound, + self.catalog_api.get_endpoint, + uuid.uuid4().hex) + + def test_delete_endpoint_returns_not_found(self): + self.assertRaises(exception.EndpointNotFound, + self.catalog_api.delete_endpoint, + uuid.uuid4().hex) + + def test_create_endpoint(self): + service = unit.new_service_ref() + self.catalog_api.create_service(service['id'], service) + + endpoint = unit.new_endpoint_ref(service_id=service['id'], + region_id=None) + self.catalog_api.create_endpoint(endpoint['id'], endpoint.copy()) + + def test_update_endpoint(self): + dummy_service_ref, endpoint_ref, dummy_disabled_endpoint_ref = ( + self._create_endpoints()) + res = self.catalog_api.update_endpoint(endpoint_ref['id'], + {'interface': 'private'}) + expected_endpoint = endpoint_ref.copy() + expected_endpoint['enabled'] = True + expected_endpoint['interface'] = 'private' + if self._legacy_endpoint_id_in_endpoint: + expected_endpoint['legacy_endpoint_id'] = None + if self._enabled_default_to_true_when_creating_endpoint: + expected_endpoint['enabled'] = True + self.assertDictEqual(expected_endpoint, res) + + def _create_endpoints(self): + # Creates a service and 2 endpoints for the service in the same region. + # The 'public' interface is enabled and the 'internal' interface is + # disabled. + + def create_endpoint(service_id, region, **kwargs): + ref = unit.new_endpoint_ref( + service_id=service_id, + region_id=region, + url='http://localhost/%s' % uuid.uuid4().hex, + **kwargs) + + self.catalog_api.create_endpoint(ref['id'], ref) + return ref + + # Create a service for use with the endpoints. + service_ref = unit.new_service_ref() + service_id = service_ref['id'] + self.catalog_api.create_service(service_id, service_ref) + + region = unit.new_region_ref() + self.catalog_api.create_region(region) + + # Create endpoints + enabled_endpoint_ref = create_endpoint(service_id, region['id']) + disabled_endpoint_ref = create_endpoint( + service_id, region['id'], enabled=False, interface='internal') + + return service_ref, enabled_endpoint_ref, disabled_endpoint_ref + + def test_list_endpoints(self): + service = unit.new_service_ref() + self.catalog_api.create_service(service['id'], service) + + expected_ids = set([uuid.uuid4().hex for _ in range(3)]) + for endpoint_id in expected_ids: + endpoint = unit.new_endpoint_ref(service_id=service['id'], + id=endpoint_id, + region_id=None) + self.catalog_api.create_endpoint(endpoint['id'], endpoint) + + endpoints = self.catalog_api.list_endpoints() + self.assertEqual(expected_ids, set(e['id'] for e in endpoints)) + + def test_get_catalog_endpoint_disabled(self): + """Get back only enabled endpoints when get the v2 catalog.""" + service_ref, enabled_endpoint_ref, dummy_disabled_endpoint_ref = ( + self._create_endpoints()) + + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + catalog = self.catalog_api.get_catalog(user_id, project_id) + + exp_entry = { + 'id': enabled_endpoint_ref['id'], + 'name': service_ref['name'], + 'publicURL': enabled_endpoint_ref['url'], + } + + region = enabled_endpoint_ref['region_id'] + self.assertEqual(exp_entry, catalog[region][service_ref['type']]) + + def test_get_v3_catalog_endpoint_disabled(self): + """Get back only enabled endpoints when get the v3 catalog.""" + enabled_endpoint_ref = self._create_endpoints()[1] + + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + catalog = self.catalog_api.get_v3_catalog(user_id, project_id) + + endpoint_ids = [x['id'] for x in catalog[0]['endpoints']] + self.assertEqual([enabled_endpoint_ref['id']], endpoint_ids) + + @unit.skip_if_cache_disabled('catalog') + def test_invalidate_cache_when_updating_endpoint(self): + service = unit.new_service_ref() + self.catalog_api.create_service(service['id'], service) + + # create an endpoint attached to the service + endpoint = unit.new_endpoint_ref(service_id=service['id'], + region_id=None) + self.catalog_api.create_endpoint(endpoint['id'], endpoint) + + # cache the endpoint + self.catalog_api.get_endpoint(endpoint['id']) + + # update the endpoint via catalog api + new_url = {'url': uuid.uuid4().hex} + self.catalog_api.update_endpoint(endpoint['id'], new_url) + + # assert that we can get the new endpoint + current_endpoint = self.catalog_api.get_endpoint(endpoint['id']) + self.assertEqual(new_url['url'], current_endpoint['url']) diff --git a/keystone-moon/keystone/tests/unit/catalog/test_core.py b/keystone-moon/keystone/tests/unit/catalog/test_core.py index 2f334bb6..b04b0bb7 100644 --- a/keystone-moon/keystone/tests/unit/catalog/test_core.py +++ b/keystone-moon/keystone/tests/unit/catalog/test_core.py @@ -10,27 +10,25 @@ # License for the specific language governing permissions and limitations # under the License. -from oslo_config import cfg +import uuid from keystone.catalog import core from keystone import exception from keystone.tests import unit -CONF = cfg.CONF - - class FormatUrlTests(unit.BaseTestCase): def test_successful_formatting(self): url_template = ('http://$(public_bind_host)s:$(admin_port)d/' - '$(tenant_id)s/$(user_id)s') + '$(tenant_id)s/$(user_id)s/$(project_id)s') + project_id = uuid.uuid4().hex values = {'public_bind_host': 'server', 'admin_port': 9090, - 'tenant_id': 'A', 'user_id': 'B'} + 'tenant_id': 'A', 'user_id': 'B', 'project_id': project_id} actual_url = core.format_url(url_template, values) - expected_url = 'http://server:9090/A/B' - self.assertEqual(actual_url, expected_url) + expected_url = 'http://server:9090/A/B/%s' % (project_id,) + self.assertEqual(expected_url, actual_url) def test_raises_malformed_on_missing_key(self): self.assertRaises(exception.MalformedEndpoint, @@ -73,7 +71,7 @@ class FormatUrlTests(unit.BaseTestCase): url_template, values) - def test_substitution_with_allowed_keyerror(self): + def test_substitution_with_allowed_tenant_keyerror(self): # No value of 'tenant_id' is passed into url_template. # mod: format_url will return None instead of raising # "MalformedEndpoint" exception. @@ -86,3 +84,17 @@ class FormatUrlTests(unit.BaseTestCase): 'user_id': 'B'} self.assertIsNone(core.format_url(url_template, values, silent_keyerror_failures=['tenant_id'])) + + def test_substitution_with_allowed_project_keyerror(self): + # No value of 'project_id' is passed into url_template. + # mod: format_url will return None instead of raising + # "MalformedEndpoint" exception. + # This is intentional behavior since we don't want to skip + # all the later endpoints once there is an URL of endpoint + # trying to replace 'project_id' with None. + url_template = ('http://$(public_bind_host)s:$(admin_port)d/' + '$(project_id)s/$(user_id)s') + values = {'public_bind_host': 'server', 'admin_port': 9090, + 'user_id': 'B'} + self.assertIsNone(core.format_url(url_template, values, + silent_keyerror_failures=['project_id'])) diff --git a/keystone-moon/keystone/tests/unit/common/test_authorization.py b/keystone-moon/keystone/tests/unit/common/test_authorization.py new file mode 100644 index 00000000..73ddbc61 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/common/test_authorization.py @@ -0,0 +1,161 @@ +# Copyright 2015 IBM Corp. +# +# 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 copy +import uuid + +from keystone.common import authorization +from keystone import exception +from keystone.federation import constants as federation_constants +from keystone.models import token_model +from keystone.tests import unit +from keystone.tests.unit import test_token_provider + + +class TestTokenToAuthContext(unit.BaseTestCase): + def test_token_is_project_scoped_with_trust(self): + # Check auth_context result when the token is project-scoped and has + # trust info. + + # SAMPLE_V3_TOKEN has OS-TRUST:trust in it. + token_data = test_token_provider.SAMPLE_V3_TOKEN + token = token_model.KeystoneToken(token_id=uuid.uuid4().hex, + token_data=token_data) + + auth_context = authorization.token_to_auth_context(token) + + self.assertEqual(token, auth_context['token']) + self.assertTrue(auth_context['is_delegated_auth']) + self.assertEqual(token_data['token']['user']['id'], + auth_context['user_id']) + self.assertEqual(token_data['token']['user']['domain']['id'], + auth_context['user_domain_id']) + self.assertEqual(token_data['token']['project']['id'], + auth_context['project_id']) + self.assertEqual(token_data['token']['project']['domain']['id'], + auth_context['project_domain_id']) + self.assertNotIn('domain_id', auth_context) + self.assertNotIn('domain_name', auth_context) + self.assertEqual(token_data['token']['OS-TRUST:trust']['id'], + auth_context['trust_id']) + self.assertEqual( + token_data['token']['OS-TRUST:trust']['trustor_user_id'], + auth_context['trustor_id']) + self.assertEqual( + token_data['token']['OS-TRUST:trust']['trustee_user_id'], + auth_context['trustee_id']) + self.assertItemsEqual( + [r['name'] for r in token_data['token']['roles']], + auth_context['roles']) + self.assertIsNone(auth_context['consumer_id']) + self.assertIsNone(auth_context['access_token_id']) + self.assertNotIn('group_ids', auth_context) + + def test_token_is_domain_scoped(self): + # Check contents of auth_context when token is domain-scoped. + token_data = copy.deepcopy(test_token_provider.SAMPLE_V3_TOKEN) + del token_data['token']['project'] + + domain_id = uuid.uuid4().hex + domain_name = uuid.uuid4().hex + token_data['token']['domain'] = {'id': domain_id, 'name': domain_name} + + token = token_model.KeystoneToken(token_id=uuid.uuid4().hex, + token_data=token_data) + + auth_context = authorization.token_to_auth_context(token) + + self.assertNotIn('project_id', auth_context) + self.assertNotIn('project_domain_id', auth_context) + + self.assertEqual(domain_id, auth_context['domain_id']) + self.assertEqual(domain_name, auth_context['domain_name']) + + def test_token_is_unscoped(self): + # Check contents of auth_context when the token is unscoped. + token_data = copy.deepcopy(test_token_provider.SAMPLE_V3_TOKEN) + del token_data['token']['project'] + + token = token_model.KeystoneToken(token_id=uuid.uuid4().hex, + token_data=token_data) + + auth_context = authorization.token_to_auth_context(token) + + self.assertNotIn('project_id', auth_context) + self.assertNotIn('project_domain_id', auth_context) + self.assertNotIn('domain_id', auth_context) + self.assertNotIn('domain_name', auth_context) + + def test_token_is_for_federated_user(self): + # When the token is for a federated user then group_ids is in + # auth_context. + token_data = copy.deepcopy(test_token_provider.SAMPLE_V3_TOKEN) + + group_ids = [uuid.uuid4().hex for x in range(1, 5)] + + federation_data = {'identity_provider': {'id': uuid.uuid4().hex}, + 'protocol': {'id': 'saml2'}, + 'groups': [{'id': gid} for gid in group_ids]} + token_data['token']['user'][federation_constants.FEDERATION] = ( + federation_data) + + token = token_model.KeystoneToken(token_id=uuid.uuid4().hex, + token_data=token_data) + + auth_context = authorization.token_to_auth_context(token) + + self.assertItemsEqual(group_ids, auth_context['group_ids']) + + def test_oauth_variables_set_for_oauth_token(self): + token_data = copy.deepcopy(test_token_provider.SAMPLE_V3_TOKEN) + access_token_id = uuid.uuid4().hex + consumer_id = uuid.uuid4().hex + token_data['token']['OS-OAUTH1'] = {'access_token_id': access_token_id, + 'consumer_id': consumer_id} + token = token_model.KeystoneToken(token_id=uuid.uuid4().hex, + token_data=token_data) + + auth_context = authorization.token_to_auth_context(token) + + self.assertEqual(access_token_id, auth_context['access_token_id']) + self.assertEqual(consumer_id, auth_context['consumer_id']) + + def test_oauth_variables_not_set(self): + token_data = copy.deepcopy(test_token_provider.SAMPLE_V3_TOKEN) + token = token_model.KeystoneToken(token_id=uuid.uuid4().hex, + token_data=token_data) + + auth_context = authorization.token_to_auth_context(token) + + self.assertIsNone(auth_context['access_token_id']) + self.assertIsNone(auth_context['consumer_id']) + + def test_token_is_not_KeystoneToken_raises_exception(self): + # If the token isn't a KeystoneToken then an UnexpectedError exception + # is raised. + self.assertRaises(exception.UnexpectedError, + authorization.token_to_auth_context, {}) + + def test_user_id_missing_in_token_raises_exception(self): + # If there's no user ID in the token then an Unauthorized + # exception is raised. + token_data = copy.deepcopy(test_token_provider.SAMPLE_V3_TOKEN) + del token_data['token']['user']['id'] + + token = token_model.KeystoneToken(token_id=uuid.uuid4().hex, + token_data=token_data) + + self.assertRaises(exception.Unauthorized, + authorization.token_to_auth_context, token) diff --git a/keystone-moon/keystone/tests/unit/common/test_ldap.py b/keystone-moon/keystone/tests/unit/common/test_ldap.py index e6e2c732..eed77286 100644 --- a/keystone-moon/keystone/tests/unit/common/test_ldap.py +++ b/keystone-moon/keystone/tests/unit/common/test_ldap.py @@ -27,6 +27,7 @@ from keystone.common.ldap import core as common_ldap_core from keystone.tests import unit from keystone.tests.unit import default_fixtures from keystone.tests.unit import fakeldap +from keystone.tests.unit.ksfixtures import database CONF = cfg.CONF @@ -195,8 +196,8 @@ class DnCompareTest(unit.BaseTestCase): def test_startswith_unicode(self): # dn_startswith accepts unicode. - child = u'cn=cn=fäké,ou=OpenStäck' - parent = 'ou=OpenStäck' + child = u'cn=fäké,ou=OpenStäck' + parent = u'ou=OpenStäck' self.assertTrue(ks_ldap.dn_startswith(child, parent)) @@ -207,6 +208,8 @@ class LDAPDeleteTreeTest(unit.TestCase): ks_ldap.register_handler('fake://', fakeldap.FakeLdapNoSubtreeDelete) + self.useFixture(database.Database(self.sql_driver_version_overrides)) + self.load_backends() self.load_fixtures(default_fixtures) @@ -226,11 +229,11 @@ class LDAPDeleteTreeTest(unit.TestCase): config_files.append(unit.dirs.tests_conf('backend_ldap.conf')) return config_files - def test_deleteTree(self): + def test_delete_tree(self): """Test manually deleting a tree. Few LDAP servers support CONTROL_DELETETREE. This test - exercises the alternate code paths in BaseLdap.deleteTree. + exercises the alternate code paths in BaseLdap.delete_tree. """ conn = self.identity_api.user.get_connection() @@ -251,7 +254,7 @@ class LDAPDeleteTreeTest(unit.TestCase): # cn=base # cn=child,cn=base # cn=grandchild,cn=child,cn=base - # then attempt to deleteTree(cn=base) + # then attempt to delete_tree(cn=base) base_id = 'base' base_dn = create_entry(base_id) child_dn = create_entry('child', base_dn) @@ -273,8 +276,8 @@ class LDAPDeleteTreeTest(unit.TestCase): self.assertRaises(ldap.NOT_ALLOWED_ON_NONLEAF, conn.delete_s, child_dn) - # call our deleteTree implementation - self.identity_api.user.deleteTree(base_id) + # call our delete_tree implementation + self.identity_api.user.delete_tree(base_id) self.assertRaises(ldap.NO_SUCH_OBJECT, conn.search_s, base_dn, ldap.SCOPE_BASE) self.assertRaises(ldap.NO_SUCH_OBJECT, @@ -283,6 +286,24 @@ class LDAPDeleteTreeTest(unit.TestCase): conn.search_s, grandchild_dn, ldap.SCOPE_BASE) +class MultiURLTests(unit.TestCase): + """Tests for setting multiple LDAP URLs.""" + + def test_multiple_urls_with_comma_no_conn_pool(self): + urls = 'ldap://localhost,ldap://backup.localhost' + self.config_fixture.config(group='ldap', url=urls, use_pool=False) + base_ldap = ks_ldap.BaseLdap(CONF) + ldap_connection = base_ldap.get_connection() + self.assertEqual(urls, ldap_connection.conn.conn._uri) + + def test_multiple_urls_with_comma_with_conn_pool(self): + urls = 'ldap://localhost,ldap://backup.localhost' + self.config_fixture.config(group='ldap', url=urls, use_pool=True) + base_ldap = ks_ldap.BaseLdap(CONF) + ldap_connection = base_ldap.get_connection() + self.assertEqual(urls, ldap_connection.conn.conn_pool.uri) + + class SslTlsTest(unit.TestCase): """Tests for the SSL/TLS functionality in keystone.common.ldap.core.""" @@ -359,6 +380,7 @@ class LDAPPagedResultsTest(unit.TestCase): ks_ldap.register_handler('fake://', fakeldap.FakeLdap) self.addCleanup(common_ldap_core._HANDLERS.clear) + self.useFixture(database.Database(self.sql_driver_version_overrides)) self.load_backends() self.load_fixtures(default_fixtures) diff --git a/keystone-moon/keystone/tests/unit/common/test_manager.py b/keystone-moon/keystone/tests/unit/common/test_manager.py index 1bc19763..7ef91e15 100644 --- a/keystone-moon/keystone/tests/unit/common/test_manager.py +++ b/keystone-moon/keystone/tests/unit/common/test_manager.py @@ -24,7 +24,7 @@ class TestCreateLegacyDriver(unit.BaseTestCase): Driver = manager.create_legacy_driver(catalog.CatalogDriverV8) # NOTE(dstanek): I want to subvert the requirement for this - # class to implement all of the abstractmethods. + # class to implement all of the abstract methods. Driver.__abstractmethods__ = set() impl = Driver() @@ -32,8 +32,9 @@ class TestCreateLegacyDriver(unit.BaseTestCase): 'as_of': 'Liberty', 'what': 'keystone.catalog.core.Driver', 'in_favor_of': 'keystone.catalog.core.CatalogDriverV8', - 'remove_in': 'N', + 'remove_in': mock.ANY, } mock_reporter.assert_called_with(mock.ANY, mock.ANY, details) + self.assertEqual('N', mock_reporter.call_args[0][2]['remove_in'][0]) self.assertIsInstance(impl, catalog.CatalogDriverV8) diff --git a/keystone-moon/keystone/tests/unit/common/test_notifications.py b/keystone-moon/keystone/tests/unit/common/test_notifications.py index 1ad8d50d..aa2e6f72 100644 --- a/keystone-moon/keystone/tests/unit/common/test_notifications.py +++ b/keystone-moon/keystone/tests/unit/common/test_notifications.py @@ -43,9 +43,7 @@ class ArbitraryException(Exception): def register_callback(operation, resource_type=EXP_RESOURCE_TYPE): - """Helper for creating and registering a mock callback. - - """ + """Helper for creating and registering a mock callback.""" callback = mock.Mock(__name__='callback', im_class=mock.Mock(__name__='class')) notifications.register_event_callback(operation, resource_type, callback) @@ -95,89 +93,14 @@ class AuditNotificationsTestCase(unit.BaseTestCase): DISABLED_OPERATION) -class NotificationsWrapperTestCase(unit.BaseTestCase): - def create_fake_ref(self): - resource_id = uuid.uuid4().hex - return resource_id, { - 'id': resource_id, - 'key': uuid.uuid4().hex - } - - @notifications.created(EXP_RESOURCE_TYPE) - def create_resource(self, resource_id, data): - return data - - def test_resource_created_notification(self): - exp_resource_id, data = self.create_fake_ref() - callback = register_callback(CREATED_OPERATION) - - self.create_resource(exp_resource_id, data) - callback.assert_called_with('identity', EXP_RESOURCE_TYPE, - CREATED_OPERATION, - {'resource_info': exp_resource_id}) - - @notifications.updated(EXP_RESOURCE_TYPE) - def update_resource(self, resource_id, data): - return data - - def test_resource_updated_notification(self): - exp_resource_id, data = self.create_fake_ref() - callback = register_callback(UPDATED_OPERATION) - - self.update_resource(exp_resource_id, data) - callback.assert_called_with('identity', EXP_RESOURCE_TYPE, - UPDATED_OPERATION, - {'resource_info': exp_resource_id}) - - @notifications.deleted(EXP_RESOURCE_TYPE) - def delete_resource(self, resource_id): - pass - - def test_resource_deleted_notification(self): - exp_resource_id = uuid.uuid4().hex - callback = register_callback(DELETED_OPERATION) - - self.delete_resource(exp_resource_id) - callback.assert_called_with('identity', EXP_RESOURCE_TYPE, - DELETED_OPERATION, - {'resource_info': exp_resource_id}) - - @notifications.created(EXP_RESOURCE_TYPE) - def create_exception(self, resource_id): - raise ArbitraryException() - - def test_create_exception_without_notification(self): - callback = register_callback(CREATED_OPERATION) - self.assertRaises( - ArbitraryException, self.create_exception, uuid.uuid4().hex) - self.assertFalse(callback.called) - - @notifications.created(EXP_RESOURCE_TYPE) - def update_exception(self, resource_id): - raise ArbitraryException() - - def test_update_exception_without_notification(self): - callback = register_callback(UPDATED_OPERATION) - self.assertRaises( - ArbitraryException, self.update_exception, uuid.uuid4().hex) - self.assertFalse(callback.called) - - @notifications.deleted(EXP_RESOURCE_TYPE) - def delete_exception(self, resource_id): - raise ArbitraryException() - - def test_delete_exception_without_notification(self): - callback = register_callback(DELETED_OPERATION) - self.assertRaises( - ArbitraryException, self.delete_exception, uuid.uuid4().hex) - self.assertFalse(callback.called) - - class NotificationsTestCase(unit.BaseTestCase): def test_send_notification(self): - """Test the private method _send_notification to ensure event_type, - payload, and context are built and passed properly. + """Test _send_notification. + + Test the private method _send_notification to ensure event_type, + payload, and context are built and passed properly. + """ resource = uuid.uuid4().hex resource_type = EXP_RESOURCE_TYPE @@ -203,6 +126,82 @@ class NotificationsTestCase(unit.BaseTestCase): resource) mocked.assert_called_once_with(*expected_args) + def test_send_notification_with_opt_out(self): + """Test the private method _send_notification with opt-out. + + Test that _send_notification does not notify when a valid + notification_opt_out configuration is provided. + """ + resource = uuid.uuid4().hex + resource_type = EXP_RESOURCE_TYPE + operation = CREATED_OPERATION + event_type = 'identity.%s.created' % resource_type + + # NOTE(diazjf): Here we add notification_opt_out to the + # configuration so that we should return before _get_notifer is + # called. This is because we are opting out notifications for the + # passed resource_type and operation. + conf = self.useFixture(config_fixture.Config(CONF)) + conf.config(notification_opt_out=event_type) + + with mock.patch.object(notifications._get_notifier(), + '_notify') as mocked: + + notifications._send_notification(operation, resource_type, + resource) + mocked.assert_not_called() + + def test_send_audit_notification_with_opt_out(self): + """Test the private method _send_audit_notification with opt-out. + + Test that _send_audit_notification does not notify when a valid + notification_opt_out configuration is provided. + """ + resource_type = EXP_RESOURCE_TYPE + + action = CREATED_OPERATION + '.' + resource_type + initiator = mock + target = mock + outcome = 'success' + event_type = 'identity.%s.created' % resource_type + + conf = self.useFixture(config_fixture.Config(CONF)) + conf.config(notification_opt_out=event_type) + + with mock.patch.object(notifications._get_notifier(), + '_notify') as mocked: + + notifications._send_audit_notification(action, + initiator, + outcome, + target, + event_type) + mocked.assert_not_called() + + def test_opt_out_authenticate_event(self): + """Test that authenticate events are successfully opted out.""" + resource_type = EXP_RESOURCE_TYPE + + action = CREATED_OPERATION + '.' + resource_type + initiator = mock + target = mock + outcome = 'success' + event_type = 'identity.authenticate' + meter_name = '%s.%s' % (event_type, outcome) + + conf = self.useFixture(config_fixture.Config(CONF)) + conf.config(notification_opt_out=meter_name) + + with mock.patch.object(notifications._get_notifier(), + '_notify') as mocked: + + notifications._send_audit_notification(action, + initiator, + outcome, + target, + event_type) + mocked.assert_not_called() + class BaseNotificationTest(test_v3.RestfulTestCase): @@ -213,13 +212,17 @@ class BaseNotificationTest(test_v3.RestfulTestCase): self._audits = [] def fake_notify(operation, resource_type, resource_id, - public=True): + actor_dict=None, public=True): note = { 'resource_id': resource_id, 'operation': operation, 'resource_type': resource_type, 'send_notification_called': True, 'public': public} + if actor_dict: + note['actor_id'] = actor_dict.get('id') + note['actor_type'] = actor_dict.get('type') + note['actor_operation'] = actor_dict.get('actor_operation') self._notifications.append(note) self.useFixture(mockpatch.PatchObject( @@ -249,17 +252,23 @@ class BaseNotificationTest(test_v3.RestfulTestCase): self.useFixture(mockpatch.PatchObject( notifications, '_send_audit_notification', fake_audit)) - def _assert_last_note(self, resource_id, operation, resource_type): + def _assert_last_note(self, resource_id, operation, resource_type, + actor_id=None, actor_type=None, + actor_operation=None): # NOTE(stevemar): If 'basic' format is not used, then simply # return since this assertion is not valid. if CONF.notification_format != 'basic': return self.assertTrue(len(self._notifications) > 0) note = self._notifications[-1] - self.assertEqual(note['operation'], operation) - self.assertEqual(note['resource_id'], resource_id) - self.assertEqual(note['resource_type'], resource_type) + self.assertEqual(operation, note['operation']) + self.assertEqual(resource_id, note['resource_id']) + self.assertEqual(resource_type, note['resource_type']) self.assertTrue(note['send_notification_called']) + if actor_id: + self.assertEqual(actor_id, note['actor_id']) + self.assertEqual(actor_type, note['actor_type']) + self.assertEqual(actor_operation, note['actor_operation']) def _assert_last_audit(self, resource_id, operation, resource_type, target_uri): @@ -318,14 +327,14 @@ class BaseNotificationTest(test_v3.RestfulTestCase): class NotificationsForEntities(BaseNotificationTest): def test_create_group(self): - group_ref = self.new_group_ref(domain_id=self.domain_id) + group_ref = unit.new_group_ref(domain_id=self.domain_id) group_ref = self.identity_api.create_group(group_ref) self._assert_last_note(group_ref['id'], CREATED_OPERATION, 'group') self._assert_last_audit(group_ref['id'], CREATED_OPERATION, 'group', cadftaxonomy.SECURITY_GROUP) def test_create_project(self): - project_ref = self.new_project_ref(domain_id=self.domain_id) + project_ref = unit.new_project_ref(domain_id=self.domain_id) self.resource_api.create_project(project_ref['id'], project_ref) self._assert_last_note( project_ref['id'], CREATED_OPERATION, 'project') @@ -333,27 +342,27 @@ class NotificationsForEntities(BaseNotificationTest): 'project', cadftaxonomy.SECURITY_PROJECT) def test_create_role(self): - role_ref = self.new_role_ref() + role_ref = unit.new_role_ref() self.role_api.create_role(role_ref['id'], role_ref) self._assert_last_note(role_ref['id'], CREATED_OPERATION, 'role') self._assert_last_audit(role_ref['id'], CREATED_OPERATION, 'role', cadftaxonomy.SECURITY_ROLE) def test_create_user(self): - user_ref = self.new_user_ref(domain_id=self.domain_id) + user_ref = unit.new_user_ref(domain_id=self.domain_id) user_ref = self.identity_api.create_user(user_ref) self._assert_last_note(user_ref['id'], CREATED_OPERATION, 'user') self._assert_last_audit(user_ref['id'], CREATED_OPERATION, 'user', cadftaxonomy.SECURITY_ACCOUNT_USER) def test_create_trust(self): - trustor = self.new_user_ref(domain_id=self.domain_id) + trustor = unit.new_user_ref(domain_id=self.domain_id) trustor = self.identity_api.create_user(trustor) - trustee = self.new_user_ref(domain_id=self.domain_id) + trustee = unit.new_user_ref(domain_id=self.domain_id) trustee = self.identity_api.create_user(trustee) - role_ref = self.new_role_ref() + role_ref = unit.new_role_ref() self.role_api.create_role(role_ref['id'], role_ref) - trust_ref = self.new_trust_ref(trustor['id'], + trust_ref = unit.new_trust_ref(trustor['id'], trustee['id']) self.trust_api.create_trust(trust_ref['id'], trust_ref, @@ -364,7 +373,7 @@ class NotificationsForEntities(BaseNotificationTest): 'OS-TRUST:trust', cadftaxonomy.SECURITY_TRUST) def test_delete_group(self): - group_ref = self.new_group_ref(domain_id=self.domain_id) + group_ref = unit.new_group_ref(domain_id=self.domain_id) group_ref = self.identity_api.create_group(group_ref) self.identity_api.delete_group(group_ref['id']) self._assert_last_note(group_ref['id'], DELETED_OPERATION, 'group') @@ -372,7 +381,7 @@ class NotificationsForEntities(BaseNotificationTest): cadftaxonomy.SECURITY_GROUP) def test_delete_project(self): - project_ref = self.new_project_ref(domain_id=self.domain_id) + project_ref = unit.new_project_ref(domain_id=self.domain_id) self.resource_api.create_project(project_ref['id'], project_ref) self.resource_api.delete_project(project_ref['id']) self._assert_last_note( @@ -381,7 +390,7 @@ class NotificationsForEntities(BaseNotificationTest): 'project', cadftaxonomy.SECURITY_PROJECT) def test_delete_role(self): - role_ref = self.new_role_ref() + role_ref = unit.new_role_ref() self.role_api.create_role(role_ref['id'], role_ref) self.role_api.delete_role(role_ref['id']) self._assert_last_note(role_ref['id'], DELETED_OPERATION, 'role') @@ -389,7 +398,7 @@ class NotificationsForEntities(BaseNotificationTest): cadftaxonomy.SECURITY_ROLE) def test_delete_user(self): - user_ref = self.new_user_ref(domain_id=self.domain_id) + user_ref = unit.new_user_ref(domain_id=self.domain_id) user_ref = self.identity_api.create_user(user_ref) self.identity_api.delete_user(user_ref['id']) self._assert_last_note(user_ref['id'], DELETED_OPERATION, 'user') @@ -397,14 +406,14 @@ class NotificationsForEntities(BaseNotificationTest): cadftaxonomy.SECURITY_ACCOUNT_USER) def test_create_domain(self): - domain_ref = self.new_domain_ref() + domain_ref = unit.new_domain_ref() self.resource_api.create_domain(domain_ref['id'], domain_ref) self._assert_last_note(domain_ref['id'], CREATED_OPERATION, 'domain') self._assert_last_audit(domain_ref['id'], CREATED_OPERATION, 'domain', cadftaxonomy.SECURITY_DOMAIN) def test_update_domain(self): - domain_ref = self.new_domain_ref() + domain_ref = unit.new_domain_ref() self.resource_api.create_domain(domain_ref['id'], domain_ref) domain_ref['description'] = uuid.uuid4().hex self.resource_api.update_domain(domain_ref['id'], domain_ref) @@ -413,7 +422,7 @@ class NotificationsForEntities(BaseNotificationTest): cadftaxonomy.SECURITY_DOMAIN) def test_delete_domain(self): - domain_ref = self.new_domain_ref() + domain_ref = unit.new_domain_ref() self.resource_api.create_domain(domain_ref['id'], domain_ref) domain_ref['enabled'] = False self.resource_api.update_domain(domain_ref['id'], domain_ref) @@ -423,12 +432,12 @@ class NotificationsForEntities(BaseNotificationTest): cadftaxonomy.SECURITY_DOMAIN) def test_delete_trust(self): - trustor = self.new_user_ref(domain_id=self.domain_id) + trustor = unit.new_user_ref(domain_id=self.domain_id) trustor = self.identity_api.create_user(trustor) - trustee = self.new_user_ref(domain_id=self.domain_id) + trustee = unit.new_user_ref(domain_id=self.domain_id) trustee = self.identity_api.create_user(trustee) - role_ref = self.new_role_ref() - trust_ref = self.new_trust_ref(trustor['id'], trustee['id']) + role_ref = unit.new_role_ref() + trust_ref = unit.new_trust_ref(trustor['id'], trustee['id']) self.trust_api.create_trust(trust_ref['id'], trust_ref, [role_ref]) @@ -439,7 +448,9 @@ class NotificationsForEntities(BaseNotificationTest): 'OS-TRUST:trust', cadftaxonomy.SECURITY_TRUST) def test_create_endpoint(self): - endpoint_ref = self.new_endpoint_ref(service_id=self.service_id) + endpoint_ref = unit.new_endpoint_ref(service_id=self.service_id, + interface='public', + region_id=self.region_id) self.catalog_api.create_endpoint(endpoint_ref['id'], endpoint_ref) self._assert_notify_sent(endpoint_ref['id'], CREATED_OPERATION, 'endpoint') @@ -447,7 +458,9 @@ class NotificationsForEntities(BaseNotificationTest): 'endpoint', cadftaxonomy.SECURITY_ENDPOINT) def test_update_endpoint(self): - endpoint_ref = self.new_endpoint_ref(service_id=self.service_id) + endpoint_ref = unit.new_endpoint_ref(service_id=self.service_id, + interface='public', + region_id=self.region_id) self.catalog_api.create_endpoint(endpoint_ref['id'], endpoint_ref) self.catalog_api.update_endpoint(endpoint_ref['id'], endpoint_ref) self._assert_notify_sent(endpoint_ref['id'], UPDATED_OPERATION, @@ -456,7 +469,9 @@ class NotificationsForEntities(BaseNotificationTest): 'endpoint', cadftaxonomy.SECURITY_ENDPOINT) def test_delete_endpoint(self): - endpoint_ref = self.new_endpoint_ref(service_id=self.service_id) + endpoint_ref = unit.new_endpoint_ref(service_id=self.service_id, + interface='public', + region_id=self.region_id) self.catalog_api.create_endpoint(endpoint_ref['id'], endpoint_ref) self.catalog_api.delete_endpoint(endpoint_ref['id']) self._assert_notify_sent(endpoint_ref['id'], DELETED_OPERATION, @@ -465,7 +480,7 @@ class NotificationsForEntities(BaseNotificationTest): 'endpoint', cadftaxonomy.SECURITY_ENDPOINT) def test_create_service(self): - service_ref = self.new_service_ref() + service_ref = unit.new_service_ref() self.catalog_api.create_service(service_ref['id'], service_ref) self._assert_notify_sent(service_ref['id'], CREATED_OPERATION, 'service') @@ -473,7 +488,7 @@ class NotificationsForEntities(BaseNotificationTest): 'service', cadftaxonomy.SECURITY_SERVICE) def test_update_service(self): - service_ref = self.new_service_ref() + service_ref = unit.new_service_ref() self.catalog_api.create_service(service_ref['id'], service_ref) self.catalog_api.update_service(service_ref['id'], service_ref) self._assert_notify_sent(service_ref['id'], UPDATED_OPERATION, @@ -482,7 +497,7 @@ class NotificationsForEntities(BaseNotificationTest): 'service', cadftaxonomy.SECURITY_SERVICE) def test_delete_service(self): - service_ref = self.new_service_ref() + service_ref = unit.new_service_ref() self.catalog_api.create_service(service_ref['id'], service_ref) self.catalog_api.delete_service(service_ref['id']) self._assert_notify_sent(service_ref['id'], DELETED_OPERATION, @@ -491,7 +506,7 @@ class NotificationsForEntities(BaseNotificationTest): 'service', cadftaxonomy.SECURITY_SERVICE) def test_create_region(self): - region_ref = self.new_region_ref() + region_ref = unit.new_region_ref() self.catalog_api.create_region(region_ref) self._assert_notify_sent(region_ref['id'], CREATED_OPERATION, 'region') @@ -499,7 +514,7 @@ class NotificationsForEntities(BaseNotificationTest): 'region', cadftaxonomy.SECURITY_REGION) def test_update_region(self): - region_ref = self.new_region_ref() + region_ref = unit.new_region_ref() self.catalog_api.create_region(region_ref) self.catalog_api.update_region(region_ref['id'], region_ref) self._assert_notify_sent(region_ref['id'], UPDATED_OPERATION, @@ -508,7 +523,7 @@ class NotificationsForEntities(BaseNotificationTest): 'region', cadftaxonomy.SECURITY_REGION) def test_delete_region(self): - region_ref = self.new_region_ref() + region_ref = unit.new_region_ref() self.catalog_api.create_region(region_ref) self.catalog_api.delete_region(region_ref['id']) self._assert_notify_sent(region_ref['id'], DELETED_OPERATION, @@ -517,7 +532,7 @@ class NotificationsForEntities(BaseNotificationTest): 'region', cadftaxonomy.SECURITY_REGION) def test_create_policy(self): - policy_ref = self.new_policy_ref() + policy_ref = unit.new_policy_ref() self.policy_api.create_policy(policy_ref['id'], policy_ref) self._assert_notify_sent(policy_ref['id'], CREATED_OPERATION, 'policy') @@ -525,7 +540,7 @@ class NotificationsForEntities(BaseNotificationTest): 'policy', cadftaxonomy.SECURITY_POLICY) def test_update_policy(self): - policy_ref = self.new_policy_ref() + policy_ref = unit.new_policy_ref() self.policy_api.create_policy(policy_ref['id'], policy_ref) self.policy_api.update_policy(policy_ref['id'], policy_ref) self._assert_notify_sent(policy_ref['id'], UPDATED_OPERATION, @@ -534,7 +549,7 @@ class NotificationsForEntities(BaseNotificationTest): 'policy', cadftaxonomy.SECURITY_POLICY) def test_delete_policy(self): - policy_ref = self.new_policy_ref() + policy_ref = unit.new_policy_ref() self.policy_api.create_policy(policy_ref['id'], policy_ref) self.policy_api.delete_policy(policy_ref['id']) self._assert_notify_sent(policy_ref['id'], DELETED_OPERATION, @@ -543,7 +558,7 @@ class NotificationsForEntities(BaseNotificationTest): 'policy', cadftaxonomy.SECURITY_POLICY) def test_disable_domain(self): - domain_ref = self.new_domain_ref() + domain_ref = unit.new_domain_ref() self.resource_api.create_domain(domain_ref['id'], domain_ref) domain_ref['enabled'] = False self.resource_api.update_domain(domain_ref['id'], domain_ref) @@ -551,8 +566,7 @@ class NotificationsForEntities(BaseNotificationTest): public=False) def test_disable_of_disabled_domain_does_not_notify(self): - domain_ref = self.new_domain_ref() - domain_ref['enabled'] = False + domain_ref = unit.new_domain_ref(enabled=False) self.resource_api.create_domain(domain_ref['id'], domain_ref) # The domain_ref above is not changed during the create process. We # can use the same ref to perform the update. @@ -561,7 +575,7 @@ class NotificationsForEntities(BaseNotificationTest): public=False) def test_update_group(self): - group_ref = self.new_group_ref(domain_id=self.domain_id) + group_ref = unit.new_group_ref(domain_id=self.domain_id) group_ref = self.identity_api.create_group(group_ref) self.identity_api.update_group(group_ref['id'], group_ref) self._assert_last_note(group_ref['id'], UPDATED_OPERATION, 'group') @@ -569,7 +583,7 @@ class NotificationsForEntities(BaseNotificationTest): cadftaxonomy.SECURITY_GROUP) def test_update_project(self): - project_ref = self.new_project_ref(domain_id=self.domain_id) + project_ref = unit.new_project_ref(domain_id=self.domain_id) self.resource_api.create_project(project_ref['id'], project_ref) self.resource_api.update_project(project_ref['id'], project_ref) self._assert_notify_sent( @@ -578,7 +592,7 @@ class NotificationsForEntities(BaseNotificationTest): 'project', cadftaxonomy.SECURITY_PROJECT) def test_disable_project(self): - project_ref = self.new_project_ref(domain_id=self.domain_id) + project_ref = unit.new_project_ref(domain_id=self.domain_id) self.resource_api.create_project(project_ref['id'], project_ref) project_ref['enabled'] = False self.resource_api.update_project(project_ref['id'], project_ref) @@ -586,8 +600,8 @@ class NotificationsForEntities(BaseNotificationTest): public=False) def test_disable_of_disabled_project_does_not_notify(self): - project_ref = self.new_project_ref(domain_id=self.domain_id) - project_ref['enabled'] = False + project_ref = unit.new_project_ref(domain_id=self.domain_id, + enabled=False) self.resource_api.create_project(project_ref['id'], project_ref) # The project_ref above is not changed during the create process. We # can use the same ref to perform the update. @@ -596,7 +610,7 @@ class NotificationsForEntities(BaseNotificationTest): public=False) def test_update_project_does_not_send_disable(self): - project_ref = self.new_project_ref(domain_id=self.domain_id) + project_ref = unit.new_project_ref(domain_id=self.domain_id) self.resource_api.create_project(project_ref['id'], project_ref) project_ref['enabled'] = True self.resource_api.update_project(project_ref['id'], project_ref) @@ -605,7 +619,7 @@ class NotificationsForEntities(BaseNotificationTest): self._assert_notify_not_sent(project_ref['id'], 'disabled', 'project') def test_update_role(self): - role_ref = self.new_role_ref() + role_ref = unit.new_role_ref() self.role_api.create_role(role_ref['id'], role_ref) self.role_api.update_role(role_ref['id'], role_ref) self._assert_last_note(role_ref['id'], UPDATED_OPERATION, 'role') @@ -613,7 +627,7 @@ class NotificationsForEntities(BaseNotificationTest): cadftaxonomy.SECURITY_ROLE) def test_update_user(self): - user_ref = self.new_user_ref(domain_id=self.domain_id) + user_ref = unit.new_user_ref(domain_id=self.domain_id) user_ref = self.identity_api.create_user(user_ref) self.identity_api.update_user(user_ref['id'], user_ref) self._assert_last_note(user_ref['id'], UPDATED_OPERATION, 'user') @@ -622,7 +636,7 @@ class NotificationsForEntities(BaseNotificationTest): def test_config_option_no_events(self): self.config_fixture.config(notification_format='basic') - role_ref = self.new_role_ref() + role_ref = unit.new_role_ref() self.role_api.create_role(role_ref['id'], role_ref) # The regular notifications will still be emitted, since they are # used for callback handling. @@ -630,6 +644,28 @@ class NotificationsForEntities(BaseNotificationTest): # No audit event should have occurred self.assertEqual(0, len(self._audits)) + def test_add_user_to_group(self): + user_ref = unit.new_user_ref(domain_id=self.domain_id) + user_ref = self.identity_api.create_user(user_ref) + group_ref = unit.new_group_ref(domain_id=self.domain_id) + group_ref = self.identity_api.create_group(group_ref) + self.identity_api.add_user_to_group(user_ref['id'], group_ref['id']) + self._assert_last_note(group_ref['id'], UPDATED_OPERATION, 'group', + actor_id=user_ref['id'], actor_type='user', + actor_operation='added') + + def test_remove_user_from_group(self): + user_ref = unit.new_user_ref(domain_id=self.domain_id) + user_ref = self.identity_api.create_user(user_ref) + group_ref = unit.new_group_ref(domain_id=self.domain_id) + group_ref = self.identity_api.create_group(group_ref) + self.identity_api.add_user_to_group(user_ref['id'], group_ref['id']) + self.identity_api.remove_user_from_group(user_ref['id'], + group_ref['id']) + self._assert_last_note(group_ref['id'], UPDATED_OPERATION, 'group', + actor_id=user_ref['id'], actor_type='user', + actor_operation='removed') + class CADFNotificationsForEntities(NotificationsForEntities): @@ -638,7 +674,7 @@ class CADFNotificationsForEntities(NotificationsForEntities): self.config_fixture.config(notification_format='cadf') def test_initiator_data_is_set(self): - ref = self.new_domain_ref() + ref = unit.new_domain_ref() resp = self.post('/domains', body={'domain': ref}) resource_id = resp.result.get('domain').get('id') self._assert_last_audit(resource_id, CREATED_OPERATION, 'domain', @@ -809,7 +845,7 @@ class TestEventCallbacks(test_v3.RestfulTestCase): def test_notification_received(self): callback = register_callback(CREATED_OPERATION, 'project') - project_ref = self.new_project_ref(domain_id=self.domain_id) + project_ref = unit.new_project_ref(domain_id=self.domain_id) self.resource_api.create_project(project_ref['id'], project_ref) self.assertTrue(callback.called) @@ -854,7 +890,7 @@ class TestEventCallbacks(test_v3.RestfulTestCase): callback_called.append(True) Foo() - project_ref = self.new_project_ref(domain_id=self.domain_id) + project_ref = unit.new_project_ref(domain_id=self.domain_id) self.resource_api.create_project(project_ref['id'], project_ref) self.assertEqual([True], callback_called) @@ -877,7 +913,7 @@ class TestEventCallbacks(test_v3.RestfulTestCase): callback_called.append('cb1') Foo() - project_ref = self.new_project_ref(domain_id=self.domain_id) + project_ref = unit.new_project_ref(domain_id=self.domain_id) self.resource_api.create_project(project_ref['id'], project_ref) self.assertItemsEqual(['cb1', 'cb0'], callback_called) @@ -919,7 +955,7 @@ class TestEventCallbacks(test_v3.RestfulTestCase): # something like: # self.assertRaises(TypeError, Foo) Foo() - project_ref = self.new_project_ref(domain_id=self.domain_id) + project_ref = unit.new_project_ref(domain_id=self.domain_id) self.assertRaises(TypeError, self.resource_api.create_project, project_ref['id'], project_ref) @@ -963,13 +999,13 @@ class CadfNotificationsWrapperTestCase(test_v3.RestfulTestCase): def _assert_last_note(self, action, user_id, event_type=None): self.assertTrue(self._notifications) note = self._notifications[-1] - self.assertEqual(note['action'], action) + self.assertEqual(action, note['action']) initiator = note['initiator'] - self.assertEqual(initiator.id, user_id) - self.assertEqual(initiator.host.address, self.LOCAL_HOST) + self.assertEqual(user_id, initiator.id) + self.assertEqual(self.LOCAL_HOST, initiator.host.address) self.assertTrue(note['send_notification_called']) if event_type: - self.assertEqual(note['event_type'], event_type) + self.assertEqual(event_type, note['event_type']) def _assert_event(self, role_id, project=None, domain=None, user=None, group=None, inherit=False): @@ -1006,7 +1042,6 @@ class CadfNotificationsWrapperTestCase(test_v3.RestfulTestCase): 'id': 'openstack:782689dd-f428-4f13-99c7-5c70f94a5ac1' } """ - note = self._notifications[-1] event = note['event'] if project: @@ -1073,7 +1108,7 @@ class CadfNotificationsWrapperTestCase(test_v3.RestfulTestCase): user=self.user_id) def test_group_domain_grant(self): - group_ref = self.new_group_ref(domain_id=self.domain_id) + group_ref = unit.new_group_ref(domain_id=self.domain_id) group = self.identity_api.create_group(group_ref) self.identity_api.add_user_to_group(self.user_id, group['id']) url = ('/domains/%s/groups/%s/roles/%s' % @@ -1087,7 +1122,7 @@ class CadfNotificationsWrapperTestCase(test_v3.RestfulTestCase): # A notification is sent when add_role_to_user_and_project is called on # the assignment manager. - project_ref = self.new_project_ref(self.domain_id) + project_ref = unit.new_project_ref(self.domain_id) project = self.resource_api.create_project( project_ref['id'], project_ref) tenant_id = project['id'] @@ -1097,7 +1132,7 @@ class CadfNotificationsWrapperTestCase(test_v3.RestfulTestCase): self.assertTrue(self._notifications) note = self._notifications[-1] - self.assertEqual(note['action'], 'created.role_assignment') + self.assertEqual('created.role_assignment', note['action']) self.assertTrue(note['send_notification_called']) self._assert_event(self.role_id, project=tenant_id, user=self.user_id) @@ -1111,7 +1146,7 @@ class CadfNotificationsWrapperTestCase(test_v3.RestfulTestCase): self.assertTrue(self._notifications) note = self._notifications[-1] - self.assertEqual(note['action'], 'deleted.role_assignment') + self.assertEqual('deleted.role_assignment', note['action']) self.assertTrue(note['send_notification_called']) self._assert_event(self.role_id, project=self.project_id, @@ -1126,7 +1161,9 @@ class TestCallbackRegistration(unit.BaseTestCase): self.mock_log.logger.getEffectiveLevel.return_value = logging.DEBUG def verify_log_message(self, data): - """Tests that use this are a little brittle because adding more + """Verify log message. + + Tests that use this are a little brittle because adding more logging can break them. TODO(dstanek): remove the need for this in a future refactoring diff --git a/keystone-moon/keystone/tests/unit/common/test_sql_core.py b/keystone-moon/keystone/tests/unit/common/test_sql_core.py index b110ed08..7d20eb03 100644 --- a/keystone-moon/keystone/tests/unit/common/test_sql_core.py +++ b/keystone-moon/keystone/tests/unit/common/test_sql_core.py @@ -32,14 +32,14 @@ class TestModelDictMixin(unit.BaseTestCase): def test_creating_a_model_instance_from_a_dict(self): d = {'id': utils.new_uuid(), 'text': utils.new_uuid()} m = TestModel.from_dict(d) - self.assertEqual(m.id, d['id']) - self.assertEqual(m.text, d['text']) + self.assertEqual(d['id'], m.id) + self.assertEqual(d['text'], m.text) def test_creating_a_dict_from_a_model_instance(self): m = TestModel(id=utils.new_uuid(), text=utils.new_uuid()) d = m.to_dict() - self.assertEqual(m.id, d['id']) - self.assertEqual(m.text, d['text']) + self.assertEqual(d['id'], m.id) + self.assertEqual(d['text'], m.text) def test_creating_a_model_instance_from_an_invalid_dict(self): d = {'id': utils.new_uuid(), 'text': utils.new_uuid(), 'extra': None} @@ -49,4 +49,4 @@ class TestModelDictMixin(unit.BaseTestCase): expected = {'id': utils.new_uuid(), 'text': utils.new_uuid()} m = TestModel(id=expected['id'], text=expected['text']) m.extra = 'this should not be in the dictionary' - self.assertEqual(m.to_dict(), expected) + self.assertEqual(expected, m.to_dict()) diff --git a/keystone-moon/keystone/tests/unit/common/test_utils.py b/keystone-moon/keystone/tests/unit/common/test_utils.py index d52eb729..3641aacd 100644 --- a/keystone-moon/keystone/tests/unit/common/test_utils.py +++ b/keystone-moon/keystone/tests/unit/common/test_utils.py @@ -1,3 +1,4 @@ +# encoding: utf-8 # 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 @@ -16,12 +17,13 @@ import uuid from oslo_config import cfg from oslo_config import fixture as config_fixture from oslo_serialization import jsonutils +import six from keystone.common import utils as common_utils from keystone import exception -from keystone import service from keystone.tests import unit from keystone.tests.unit import utils +from keystone.version import service CONF = cfg.CONF @@ -36,6 +38,38 @@ class UtilsTestCase(unit.BaseTestCase): super(UtilsTestCase, self).setUp() self.config_fixture = self.useFixture(config_fixture.Config(CONF)) + def test_resource_uuid(self): + uuid_str = '536e28c2017e405e89b25a1ed777b952' + self.assertEqual(uuid_str, common_utils.resource_uuid(uuid_str)) + + # Exact 64 length string. + uuid_str = ('536e28c2017e405e89b25a1ed777b952' + 'f13de678ac714bb1b7d1e9a007c10db5') + resource_id_namespace = common_utils.RESOURCE_ID_NAMESPACE + transformed_id = uuid.uuid5(resource_id_namespace, uuid_str).hex + self.assertEqual(transformed_id, common_utils.resource_uuid(uuid_str)) + + # Non-ASCII character test. + non_ascii_ = 'ß' * 32 + transformed_id = uuid.uuid5(resource_id_namespace, non_ascii_).hex + self.assertEqual(transformed_id, + common_utils.resource_uuid(non_ascii_)) + + # This input is invalid because it's length is more than 64. + invalid_input = 'x' * 65 + self.assertRaises(ValueError, common_utils.resource_uuid, + invalid_input) + + # 64 length unicode string, to mimic what is returned from mapping_id + # backend. + uuid_str = six.text_type('536e28c2017e405e89b25a1ed777b952' + 'f13de678ac714bb1b7d1e9a007c10db5') + resource_id_namespace = common_utils.RESOURCE_ID_NAMESPACE + if six.PY2: + uuid_str = uuid_str.encode('utf-8') + transformed_id = uuid.uuid5(resource_id_namespace, uuid_str).hex + self.assertEqual(transformed_id, common_utils.resource_uuid(uuid_str)) + def test_hash(self): password = 'right' wrong = 'wrongwrong' # Two wrongs don't make a right @@ -153,6 +187,18 @@ class UtilsTestCase(unit.BaseTestCase): expected_json = '{"field":"value"}' self.assertEqual(expected_json, json) + def test_url_safe_check(self): + base_str = 'i am safe' + self.assertFalse(common_utils.is_not_url_safe(base_str)) + for i in common_utils.URL_RESERVED_CHARS: + self.assertTrue(common_utils.is_not_url_safe(base_str + i)) + + def test_url_safe_with_unicode_check(self): + base_str = u'i am \xe7afe' + self.assertFalse(common_utils.is_not_url_safe(base_str)) + for i in common_utils.URL_RESERVED_CHARS: + self.assertTrue(common_utils.is_not_url_safe(base_str + i)) + class ServiceHelperTests(unit.BaseTestCase): diff --git a/keystone-moon/keystone/tests/unit/config_files/backend_ldap_sql.conf b/keystone-moon/keystone/tests/unit/config_files/backend_ldap_sql.conf index 2097b68b..96a0ffa9 100644 --- a/keystone-moon/keystone/tests/unit/config_files/backend_ldap_sql.conf +++ b/keystone-moon/keystone/tests/unit/config_files/backend_ldap_sql.conf @@ -1,5 +1,5 @@ [database] -#For a specific location file based sqlite use: +#For a specific location file based SQLite use: #connection = sqlite:////tmp/keystone.db #To Test MySQL: #connection = mysql+pymysql://keystone:keystone@localhost/keystone?charset=utf8 diff --git a/keystone-moon/keystone/tests/unit/config_files/backend_liveldap.conf b/keystone-moon/keystone/tests/unit/config_files/backend_liveldap.conf index 59cb8577..bb9ee08f 100644 --- a/keystone-moon/keystone/tests/unit/config_files/backend_liveldap.conf +++ b/keystone-moon/keystone/tests/unit/config_files/backend_liveldap.conf @@ -4,11 +4,7 @@ user = cn=Manager,dc=openstack,dc=org password = test suffix = dc=openstack,dc=org group_tree_dn = ou=UserGroups,dc=openstack,dc=org -role_tree_dn = ou=Roles,dc=openstack,dc=org -project_tree_dn = ou=Projects,dc=openstack,dc=org user_tree_dn = ou=Users,dc=openstack,dc=org -project_enabled_emulation = True user_enabled_emulation = True user_mail_attribute = mail use_dumb_member = True - diff --git a/keystone-moon/keystone/tests/unit/config_files/backend_mysql.conf b/keystone-moon/keystone/tests/unit/config_files/backend_mysql.conf index 142ca203..2495f036 100644 --- a/keystone-moon/keystone/tests/unit/config_files/backend_mysql.conf +++ b/keystone-moon/keystone/tests/unit/config_files/backend_mysql.conf @@ -1,4 +1,4 @@ -#Used for running the Migrate tests against a live Mysql Server +#Used for running the Migrate tests against a live MySQL Server #See _sql_livetest.py [database] connection = mysql+pymysql://keystone:keystone@localhost/keystone_test?charset=utf8 diff --git a/keystone-moon/keystone/tests/unit/config_files/backend_pool_liveldap.conf b/keystone-moon/keystone/tests/unit/config_files/backend_pool_liveldap.conf index a85f5226..c36e05f9 100644 --- a/keystone-moon/keystone/tests/unit/config_files/backend_pool_liveldap.conf +++ b/keystone-moon/keystone/tests/unit/config_files/backend_pool_liveldap.conf @@ -4,10 +4,7 @@ user = cn=Manager,dc=openstack,dc=org password = test suffix = dc=openstack,dc=org group_tree_dn = ou=UserGroups,dc=openstack,dc=org -role_tree_dn = ou=Roles,dc=openstack,dc=org -project_tree_dn = ou=Projects,dc=openstack,dc=org user_tree_dn = ou=Users,dc=openstack,dc=org -project_enabled_emulation = True user_enabled_emulation = True user_mail_attribute = mail use_dumb_member = True diff --git a/keystone-moon/keystone/tests/unit/config_files/backend_sql.conf b/keystone-moon/keystone/tests/unit/config_files/backend_sql.conf index 063177bd..f2828e2e 100644 --- a/keystone-moon/keystone/tests/unit/config_files/backend_sql.conf +++ b/keystone-moon/keystone/tests/unit/config_files/backend_sql.conf @@ -1,5 +1,5 @@ [database] -#For a specific location file based sqlite use: +#For a specific location file based SQLite use: #connection = sqlite:////tmp/keystone.db #To Test MySQL: #connection = mysql+pymysql://keystone:keystone@localhost/keystone?charset=utf8 diff --git a/keystone-moon/keystone/tests/unit/config_files/backend_tls_liveldap.conf b/keystone-moon/keystone/tests/unit/config_files/backend_tls_liveldap.conf index d35b9139..b66044b7 100644 --- a/keystone-moon/keystone/tests/unit/config_files/backend_tls_liveldap.conf +++ b/keystone-moon/keystone/tests/unit/config_files/backend_tls_liveldap.conf @@ -4,10 +4,7 @@ user = dc=Manager,dc=openstack,dc=org password = test suffix = dc=openstack,dc=org group_tree_dn = ou=UserGroups,dc=openstack,dc=org -role_tree_dn = ou=Roles,dc=openstack,dc=org -project_tree_dn = ou=Projects,dc=openstack,dc=org user_tree_dn = ou=Users,dc=openstack,dc=org -project_enabled_emulation = True user_enabled_emulation = True user_mail_attribute = mail use_dumb_member = True diff --git a/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.Default.conf b/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.Default.conf index 2dd86c25..64d01d48 100644 --- a/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.Default.conf +++ b/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.Default.conf @@ -11,4 +11,4 @@ password = password suffix = cn=example,cn=com [identity] -driver = ldap \ No newline at end of file +driver = ldap diff --git a/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.domain1.conf b/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.domain1.conf index ba22cdf9..af540537 100644 --- a/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.domain1.conf +++ b/keystone-moon/keystone/tests/unit/config_files/domain_configs_multi_ldap/keystone.domain1.conf @@ -8,4 +8,5 @@ password = password suffix = cn=example,cn=com [identity] -driver = ldap \ No newline at end of file +driver = ldap +list_limit = 101 diff --git a/keystone-moon/keystone/tests/unit/contrib/federation/test_utils.py b/keystone-moon/keystone/tests/unit/contrib/federation/test_utils.py index 5804f1c0..52a6095b 100644 --- a/keystone-moon/keystone/tests/unit/contrib/federation/test_utils.py +++ b/keystone-moon/keystone/tests/unit/contrib/federation/test_utils.py @@ -12,13 +12,20 @@ import uuid +from oslo_config import cfg +from oslo_config import fixture as config_fixture +from oslo_serialization import jsonutils + from keystone.auth.plugins import mapped -from keystone.contrib.federation import utils as mapping_utils from keystone import exception +from keystone.federation import utils as mapping_utils from keystone.tests import unit from keystone.tests.unit import mapping_fixtures +FAKE_MAPPING_ID = uuid.uuid4().hex + + class MappingRuleEngineTests(unit.BaseTestCase): """A class for testing the mapping rule engine.""" @@ -50,10 +57,9 @@ class MappingRuleEngineTests(unit.BaseTestCase): a direct mapping for the users name. """ - mapping = mapping_fixtures.MAPPING_LARGE assertion = mapping_fixtures.ADMIN_ASSERTION - rp = mapping_utils.RuleProcessor(mapping['rules']) + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) values = rp.process(assertion) fn = assertion.get('FirstName') @@ -71,18 +77,15 @@ class MappingRuleEngineTests(unit.BaseTestCase): This will not match since the email in the assertion will fail the regex test. It is set to match any @example.com address. But the incoming value is set to eviltester@example.org. - RuleProcessor should return list of empty group_ids. + RuleProcessor should raise ValidationError. """ - mapping = mapping_fixtures.MAPPING_LARGE assertion = mapping_fixtures.BAD_TESTER_ASSERTION - rp = mapping_utils.RuleProcessor(mapping['rules']) - mapped_properties = rp.process(assertion) - - self.assertValidMappedUserObject(mapped_properties) - self.assertIsNone(mapped_properties['user'].get('name')) - self.assertListEqual(list(), mapped_properties['group_ids']) + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) + self.assertRaises(exception.ValidationError, + rp.process, + assertion) def test_rule_engine_regex_many_groups(self): """Should return group CONTRACTOR_GROUP_ID. @@ -93,10 +96,9 @@ class MappingRuleEngineTests(unit.BaseTestCase): a match. """ - mapping = mapping_fixtures.MAPPING_TESTER_REGEX assertion = mapping_fixtures.TESTER_ASSERTION - rp = mapping_utils.RuleProcessor(mapping['rules']) + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) values = rp.process(assertion) self.assertValidMappedUserObject(values) @@ -116,10 +118,9 @@ class MappingRuleEngineTests(unit.BaseTestCase): mapping. """ - mapping = mapping_fixtures.MAPPING_SMALL assertion = mapping_fixtures.CONTRACTOR_ASSERTION - rp = mapping_utils.RuleProcessor(mapping['rules']) + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) values = rp.process(assertion) self.assertValidMappedUserObject(values) @@ -138,10 +139,9 @@ class MappingRuleEngineTests(unit.BaseTestCase): has `not_any_of`, and direct mapping to a username, no group. """ - mapping = mapping_fixtures.MAPPING_LARGE assertion = mapping_fixtures.CUSTOMER_ASSERTION - rp = mapping_utils.RuleProcessor(mapping['rules']) + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) values = rp.process(assertion) self.assertValidMappedUserObject(values) @@ -160,10 +160,9 @@ class MappingRuleEngineTests(unit.BaseTestCase): rules must be matched, including a `not_any_of`. """ - mapping = mapping_fixtures.MAPPING_SMALL assertion = mapping_fixtures.EMPLOYEE_ASSERTION - rp = mapping_utils.RuleProcessor(mapping['rules']) + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) values = rp.process(assertion) self.assertValidMappedUserObject(values) @@ -183,10 +182,9 @@ class MappingRuleEngineTests(unit.BaseTestCase): regex set to True. """ - mapping = mapping_fixtures.MAPPING_DEVELOPER_REGEX assertion = mapping_fixtures.DEVELOPER_ASSERTION - rp = mapping_utils.RuleProcessor(mapping['rules']) + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) values = rp.process(assertion) self.assertValidMappedUserObject(values) @@ -203,18 +201,15 @@ class MappingRuleEngineTests(unit.BaseTestCase): The email in the assertion will fail the regex test. It is set to reject any @example.org address, but the incoming value is set to evildeveloper@example.org. - RuleProcessor should return list of empty group_ids. + RuleProcessor should yield ValidationError. """ - mapping = mapping_fixtures.MAPPING_DEVELOPER_REGEX assertion = mapping_fixtures.BAD_DEVELOPER_ASSERTION - rp = mapping_utils.RuleProcessor(mapping['rules']) - mapped_properties = rp.process(assertion) - - self.assertValidMappedUserObject(mapped_properties) - self.assertIsNone(mapped_properties['user'].get('name')) - self.assertListEqual(list(), mapped_properties['group_ids']) + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) + self.assertRaises(exception.ValidationError, + rp.process, + assertion) def _rule_engine_regex_match_and_many_groups(self, assertion): """Should return group DEVELOPER_GROUP_ID and TESTER_GROUP_ID. @@ -223,9 +218,8 @@ class MappingRuleEngineTests(unit.BaseTestCase): Expect DEVELOPER_GROUP_ID and TESTER_GROUP_ID in the results. """ - mapping = mapping_fixtures.MAPPING_LARGE - rp = mapping_utils.RuleProcessor(mapping['rules']) + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) values = rp.process(assertion) user_name = assertion.get('UserName') @@ -265,16 +259,29 @@ class MappingRuleEngineTests(unit.BaseTestCase): Expect RuleProcessor to discard non string object, which is required for a correct rule match. RuleProcessor will result with - empty list of groups. + ValidationError. """ mapping = mapping_fixtures.MAPPING_SMALL - rp = mapping_utils.RuleProcessor(mapping['rules']) + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) assertion = mapping_fixtures.CONTRACTOR_MALFORMED_ASSERTION - mapped_properties = rp.process(assertion) - self.assertValidMappedUserObject(mapped_properties) - self.assertIsNone(mapped_properties['user'].get('name')) - self.assertListEqual(list(), mapped_properties['group_ids']) + self.assertRaises(exception.ValidationError, + rp.process, + assertion) + + def test_using_remote_direct_mapping_that_doesnt_exist_fails(self): + """Test for the correct error when referring to a bad remote match. + + The remote match must exist in a rule when a local section refers to + a remote matching using the format (e.g. {0} in a local section). + """ + mapping = mapping_fixtures.MAPPING_DIRECT_MAPPING_THROUGH_KEYWORD + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) + assertion = mapping_fixtures.CUSTOMER_ASSERTION + + self.assertRaises(exception.DirectMappingError, + rp.process, + assertion) def test_rule_engine_returns_group_names(self): """Check whether RuleProcessor returns group names with their domains. @@ -285,7 +292,7 @@ class MappingRuleEngineTests(unit.BaseTestCase): """ mapping = mapping_fixtures.MAPPING_GROUP_NAMES - rp = mapping_utils.RuleProcessor(mapping['rules']) + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) assertion = mapping_fixtures.EMPLOYEE_ASSERTION mapped_properties = rp.process(assertion) self.assertIsNotNone(mapped_properties) @@ -317,10 +324,9 @@ class MappingRuleEngineTests(unit.BaseTestCase): correctly filters out Manager and only allows Developer and Contractor. """ - mapping = mapping_fixtures.MAPPING_GROUPS_WHITELIST assertion = mapping_fixtures.EMPLOYEE_ASSERTION_MULTIPLE_GROUPS - rp = mapping_utils.RuleProcessor(mapping['rules']) + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) mapped_properties = rp.process(assertion) self.assertIsNotNone(mapped_properties) @@ -354,10 +360,9 @@ class MappingRuleEngineTests(unit.BaseTestCase): correctly filters out Manager and Developer and only allows Contractor. """ - mapping = mapping_fixtures.MAPPING_GROUPS_BLACKLIST assertion = mapping_fixtures.EMPLOYEE_ASSERTION_MULTIPLE_GROUPS - rp = mapping_utils.RuleProcessor(mapping['rules']) + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) mapped_properties = rp.process(assertion) self.assertIsNotNone(mapped_properties) @@ -383,10 +388,9 @@ class MappingRuleEngineTests(unit.BaseTestCase): entry in the remote rules. """ - mapping = mapping_fixtures.MAPPING_GROUPS_BLACKLIST_MULTIPLES assertion = mapping_fixtures.EMPLOYEE_ASSERTION_MULTIPLE_GROUPS - rp = mapping_utils.RuleProcessor(mapping['rules']) + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) mapped_properties = rp.process(assertion) self.assertIsNotNone(mapped_properties) @@ -412,7 +416,7 @@ class MappingRuleEngineTests(unit.BaseTestCase): """ mapping = mapping_fixtures.MAPPING_GROUPS_WHITELIST_MISSING_DOMAIN assertion = mapping_fixtures.EMPLOYEE_ASSERTION_MULTIPLE_GROUPS - rp = mapping_utils.RuleProcessor(mapping['rules']) + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) self.assertRaises(exception.ValidationError, rp.process, assertion) def test_rule_engine_blacklist_direct_group_mapping_missing_domain(self): @@ -423,7 +427,7 @@ class MappingRuleEngineTests(unit.BaseTestCase): """ mapping = mapping_fixtures.MAPPING_GROUPS_BLACKLIST_MISSING_DOMAIN assertion = mapping_fixtures.EMPLOYEE_ASSERTION_MULTIPLE_GROUPS - rp = mapping_utils.RuleProcessor(mapping['rules']) + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) self.assertRaises(exception.ValidationError, rp.process, assertion) def test_rule_engine_no_groups_allowed(self): @@ -436,7 +440,7 @@ class MappingRuleEngineTests(unit.BaseTestCase): """ mapping = mapping_fixtures.MAPPING_GROUPS_WHITELIST assertion = mapping_fixtures.EMPLOYEE_ASSERTION - rp = mapping_utils.RuleProcessor(mapping['rules']) + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) mapped_properties = rp.process(assertion) self.assertIsNotNone(mapped_properties) self.assertListEqual(mapped_properties['group_names'], []) @@ -444,41 +448,19 @@ class MappingRuleEngineTests(unit.BaseTestCase): self.assertEqual('tbo', mapped_properties['user']['name']) def test_mapping_federated_domain_specified(self): - """Test mapping engine when domain 'ephemeral' is explicitely set. + """Test mapping engine when domain 'ephemeral' is explicitly set. For that, we use mapping rule MAPPING_EPHEMERAL_USER and assertion EMPLOYEE_ASSERTION """ mapping = mapping_fixtures.MAPPING_EPHEMERAL_USER - rp = mapping_utils.RuleProcessor(mapping['rules']) + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) assertion = mapping_fixtures.EMPLOYEE_ASSERTION mapped_properties = rp.process(assertion) self.assertIsNotNone(mapped_properties) self.assertValidMappedUserObject(mapped_properties) - def test_create_user_object_with_bad_mapping(self): - """Test if user object is created even with bad mapping. - - User objects will be created by mapping engine always as long as there - is corresponding local rule. This test shows, that even with assertion - where no group names nor ids are matched, but there is 'blind' rule for - mapping user, such object will be created. - - In this test MAPPING_EHPEMERAL_USER expects UserName set to jsmith - whereas value from assertion is 'tbo'. - - """ - mapping = mapping_fixtures.MAPPING_EPHEMERAL_USER - rp = mapping_utils.RuleProcessor(mapping['rules']) - assertion = mapping_fixtures.CONTRACTOR_ASSERTION - mapped_properties = rp.process(assertion) - self.assertIsNotNone(mapped_properties) - self.assertValidMappedUserObject(mapped_properties) - - self.assertNotIn('id', mapped_properties['user']) - self.assertNotIn('name', mapped_properties['user']) - def test_set_ephemeral_domain_to_ephemeral_users(self): """Test auto assigning service domain to ephemeral users. @@ -488,7 +470,7 @@ class MappingRuleEngineTests(unit.BaseTestCase): """ mapping = mapping_fixtures.MAPPING_EPHEMERAL_USER_LOCAL_DOMAIN - rp = mapping_utils.RuleProcessor(mapping['rules']) + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) assertion = mapping_fixtures.CONTRACTOR_ASSERTION mapped_properties = rp.process(assertion) self.assertIsNotNone(mapped_properties) @@ -497,7 +479,7 @@ class MappingRuleEngineTests(unit.BaseTestCase): def test_local_user_local_domain(self): """Test that local users can have non-service domains assigned.""" mapping = mapping_fixtures.MAPPING_LOCAL_USER_LOCAL_DOMAIN - rp = mapping_utils.RuleProcessor(mapping['rules']) + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) assertion = mapping_fixtures.CONTRACTOR_ASSERTION mapped_properties = rp.process(assertion) self.assertIsNotNone(mapped_properties) @@ -514,19 +496,21 @@ class MappingRuleEngineTests(unit.BaseTestCase): - Check if the user has proper domain ('federated') set - Check if the user has property type set ('ephemeral') - Check if user's name is properly mapped from the assertion - - Check if user's id is properly set and equal to name, as it was not - explicitely specified in the mapping. + - Check if unique_id is properly set and equal to display_name, + as it was not explicitly specified in the mapping. """ mapping = mapping_fixtures.MAPPING_USER_IDS - rp = mapping_utils.RuleProcessor(mapping['rules']) + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) assertion = mapping_fixtures.CONTRACTOR_ASSERTION mapped_properties = rp.process(assertion) self.assertIsNotNone(mapped_properties) self.assertValidMappedUserObject(mapped_properties) - mapped.setup_username({}, mapped_properties) - self.assertEqual('jsmith', mapped_properties['user']['id']) self.assertEqual('jsmith', mapped_properties['user']['name']) + unique_id, display_name = mapped.get_user_unique_id_and_display_name( + {}, mapped_properties) + self.assertEqual('jsmith', unique_id) + self.assertEqual('jsmith', display_name) def test_user_identifications_name_and_federated_domain(self): """Test varius mapping options and how users are identified. @@ -537,20 +521,19 @@ class MappingRuleEngineTests(unit.BaseTestCase): - Check if the user has proper domain ('federated') set - Check if the user has propert type set ('ephemeral') - Check if user's name is properly mapped from the assertion - - Check if user's id is properly set and equal to name, as it was not - explicitely specified in the mapping. + - Check if the unique_id and display_name are properly set """ mapping = mapping_fixtures.MAPPING_USER_IDS - rp = mapping_utils.RuleProcessor(mapping['rules']) + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) assertion = mapping_fixtures.EMPLOYEE_ASSERTION mapped_properties = rp.process(assertion) self.assertIsNotNone(mapped_properties) self.assertValidMappedUserObject(mapped_properties) - mapped.setup_username({}, mapped_properties) - self.assertEqual('tbo', mapped_properties['user']['name']) - self.assertEqual('abc123%40example.com', - mapped_properties['user']['id']) + unique_id, display_name = mapped.get_user_unique_id_and_display_name( + {}, mapped_properties) + self.assertEqual('tbo', display_name) + self.assertEqual('abc123%40example.com', unique_id) def test_user_identification_id(self): """Test varius mapping options and how users are identified. @@ -560,21 +543,21 @@ class MappingRuleEngineTests(unit.BaseTestCase): Test plan: - Check if the user has proper domain ('federated') set - Check if the user has propert type set ('ephemeral') - - Check if user's id is properly mapped from the assertion - - Check if user's name is properly set and equal to id, as it was not - explicitely specified in the mapping. + - Check if user's display_name is properly set and equal to unique_id, + as it was not explicitly specified in the mapping. """ mapping = mapping_fixtures.MAPPING_USER_IDS - rp = mapping_utils.RuleProcessor(mapping['rules']) + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) assertion = mapping_fixtures.ADMIN_ASSERTION mapped_properties = rp.process(assertion) context = {'environment': {}} self.assertIsNotNone(mapped_properties) self.assertValidMappedUserObject(mapped_properties) - mapped.setup_username(context, mapped_properties) - self.assertEqual('bob', mapped_properties['user']['name']) - self.assertEqual('bob', mapped_properties['user']['id']) + unique_id, display_name = mapped.get_user_unique_id_and_display_name( + context, mapped_properties) + self.assertEqual('bob', unique_id) + self.assertEqual('bob', display_name) def test_user_identification_id_and_name(self): """Test varius mapping options and how users are identified. @@ -584,8 +567,8 @@ class MappingRuleEngineTests(unit.BaseTestCase): Test plan: - Check if the user has proper domain ('federated') set - Check if the user has proper type set ('ephemeral') - - Check if user's name is properly mapped from the assertion - - Check if user's id is properly set and and equal to value hardcoded + - Check if display_name is properly set from the assertion + - Check if unique_id is properly set and and equal to value hardcoded in the mapping This test does two iterations with different assertions used as input @@ -601,19 +584,21 @@ class MappingRuleEngineTests(unit.BaseTestCase): (mapping_fixtures.EMPLOYEE_ASSERTION, 'tbo')] for assertion, exp_user_name in testcases: mapping = mapping_fixtures.MAPPING_USER_IDS - rp = mapping_utils.RuleProcessor(mapping['rules']) + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) mapped_properties = rp.process(assertion) context = {'environment': {}} self.assertIsNotNone(mapped_properties) self.assertValidMappedUserObject(mapped_properties) - mapped.setup_username(context, mapped_properties) - self.assertEqual(exp_user_name, mapped_properties['user']['name']) - self.assertEqual('abc123%40example.com', - mapped_properties['user']['id']) + unique_id, display_name = ( + mapped.get_user_unique_id_and_display_name(context, + mapped_properties) + ) + self.assertEqual(exp_user_name, display_name) + self.assertEqual('abc123%40example.com', unique_id) def test_whitelist_pass_through(self): mapping = mapping_fixtures.MAPPING_GROUPS_WHITELIST_PASS_THROUGH - rp = mapping_utils.RuleProcessor(mapping['rules']) + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) assertion = mapping_fixtures.DEVELOPER_ASSERTION mapped_properties = rp.process(assertion) self.assertValidMappedUserObject(mapped_properties) @@ -622,13 +607,119 @@ class MappingRuleEngineTests(unit.BaseTestCase): self.assertEqual('Developer', mapped_properties['group_names'][0]['name']) + def test_mapping_with_incorrect_local_keys(self): + mapping = mapping_fixtures.MAPPING_BAD_LOCAL_SETUP + self.assertRaises(exception.ValidationError, + mapping_utils.validate_mapping_structure, + mapping) + + def test_mapping_with_group_name_and_domain(self): + mapping = mapping_fixtures.MAPPING_GROUP_NAMES + mapping_utils.validate_mapping_structure(mapping) + def test_type_not_in_assertion(self): """Test that if the remote "type" is not in the assertion it fails.""" mapping = mapping_fixtures.MAPPING_GROUPS_WHITELIST_PASS_THROUGH - rp = mapping_utils.RuleProcessor(mapping['rules']) + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) assertion = {uuid.uuid4().hex: uuid.uuid4().hex} + self.assertRaises(exception.ValidationError, + rp.process, + assertion) + + def test_rule_engine_group_ids_mapping_whitelist(self): + """Test mapping engine when group_ids is explicitly set + + Also test whitelists on group ids + + """ + mapping = mapping_fixtures.MAPPING_GROUPS_IDS_WHITELIST + assertion = mapping_fixtures.GROUP_IDS_ASSERTION + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) mapped_properties = rp.process(assertion) - self.assertValidMappedUserObject(mapped_properties) + self.assertIsNotNone(mapped_properties) + self.assertEqual('opilotte', mapped_properties['user']['name']) + self.assertListEqual([], mapped_properties['group_names']) + self.assertItemsEqual(['abc123', 'ghi789', 'klm012'], + mapped_properties['group_ids']) - self.assertNotIn('id', mapped_properties['user']) - self.assertNotIn('name', mapped_properties['user']) + def test_rule_engine_group_ids_mapping_blacklist(self): + """Test mapping engine when group_ids is explicitly set. + + Also test blacklists on group ids + + """ + mapping = mapping_fixtures.MAPPING_GROUPS_IDS_BLACKLIST + assertion = mapping_fixtures.GROUP_IDS_ASSERTION + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + self.assertEqual('opilotte', mapped_properties['user']['name']) + self.assertListEqual([], mapped_properties['group_names']) + self.assertItemsEqual(['abc123', 'ghi789', 'klm012'], + mapped_properties['group_ids']) + + def test_rule_engine_group_ids_mapping_only_one_group(self): + """Test mapping engine when group_ids is explicitly set. + + If the group ids list has only one group, + test if the transformation is done correctly + + """ + mapping = mapping_fixtures.MAPPING_GROUPS_IDS_WHITELIST + assertion = mapping_fixtures.GROUP_IDS_ASSERTION_ONLY_ONE_GROUP + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) + mapped_properties = rp.process(assertion) + self.assertIsNotNone(mapped_properties) + self.assertEqual('opilotte', mapped_properties['user']['name']) + self.assertListEqual([], mapped_properties['group_names']) + self.assertItemsEqual(['210mlk', '321cba'], + mapped_properties['group_ids']) + + +class TestUnicodeAssertionData(unit.BaseTestCase): + """Ensure that unicode data in the assertion headers works. + + Bug #1525250 reported that something was not getting correctly encoded + and/or decoded when assertion data contained non-ASCII characters. + + This test class mimics what happens in a real HTTP request. + """ + + def setUp(self): + super(TestUnicodeAssertionData, self).setUp() + self.config_fixture = self.useFixture(config_fixture.Config(cfg.CONF)) + self.config_fixture.config(group='federation', + assertion_prefix='PFX') + + def _pull_mapping_rules_from_the_database(self): + # NOTE(dstanek): In a live system. The rules are dumped into JSON bytes + # before being # stored in the database. Upon retrieval the bytes are + # loaded and the resulting dictionary is full of unicode text strings. + # Most of tests in this file incorrectly assume the mapping fixture + # dictionary is the same as what it would look like coming out of the + # database. The string, when coming out of the database, are all text. + return jsonutils.loads(jsonutils.dumps( + mapping_fixtures.MAPPING_UNICODE)) + + def _pull_assertion_from_the_request_headers(self): + # NOTE(dstanek): In a live system the bytes for the assertion are + # pulled from the HTTP headers. These bytes may be decodable as + # ISO-8859-1 according to Section 3.2.4 of RFC 7230. Let's assume + # that our web server plugins are correctly encoding the data. + context = dict(environment=mapping_fixtures.UNICODE_NAME_ASSERTION) + data = mapping_utils.get_assertion_params_from_env(context) + # NOTE(dstanek): keystone.auth.plugins.mapped + return dict(data) + + def test_unicode(self): + mapping = self._pull_mapping_rules_from_the_database() + assertion = self._pull_assertion_from_the_request_headers() + + rp = mapping_utils.RuleProcessor(FAKE_MAPPING_ID, mapping['rules']) + values = rp.process(assertion) + + fn = assertion.get('PFX_FirstName') + ln = assertion.get('PFX_LastName') + full_name = '%s %s' % (fn, ln) + user_name = values.get('user', {}).get('name') + self.assertEqual(full_name, user_name) diff --git a/keystone-moon/keystone/tests/unit/core.py b/keystone-moon/keystone/tests/unit/core.py index eb8b9f65..1054e131 100644 --- a/keystone-moon/keystone/tests/unit/core.py +++ b/keystone-moon/keystone/tests/unit/core.py @@ -14,8 +14,11 @@ from __future__ import absolute_import import atexit +import base64 import datetime import functools +import hashlib +import json import logging import os import re @@ -28,14 +31,16 @@ import warnings import fixtures from oslo_config import cfg from oslo_config import fixture as config_fixture +from oslo_context import context as oslo_context +from oslo_context import fixture as oslo_ctx_fixture from oslo_log import fixture as log_fixture from oslo_log import log from oslo_utils import timeutils -import oslotest.base as oslotest from oslotest import mockpatch from paste.deploy import loadwsgi import six from sqlalchemy import exc +import testtools from testtools import testcase # NOTE(ayoung) @@ -45,24 +50,20 @@ from keystone.common import environment # noqa environment.use_eventlet() from keystone import auth -from keystone.common import config as common_cfg +from keystone.common import config from keystone.common import dependency -from keystone.common import kvs from keystone.common.kvs import core as kvs_core from keystone.common import sql -from keystone import config -from keystone import controllers from keystone import exception from keystone import notifications -from keystone.policy.backends import rules from keystone.server import common -from keystone import service from keystone.tests.unit import ksfixtures +from keystone.version import controllers +from keystone.version import service config.configure() -LOG = log.getLogger(__name__) PID = six.text_type(os.getpid()) TESTSDIR = os.path.dirname(os.path.abspath(__file__)) TESTCONF = os.path.join(TESTSDIR, 'config_files') @@ -82,7 +83,6 @@ TMPDIR = _calc_tmpdir() CONF = cfg.CONF log.register_options(CONF) -rules.init() IN_MEM_DB_CONN_STRING = 'sqlite://' @@ -208,6 +208,22 @@ def skip_if_cache_disabled(*sections): return wrapper +def skip_if_cache_is_enabled(*sections): + def wrapper(f): + @functools.wraps(f) + def inner(*args, **kwargs): + if CONF.cache.enabled: + for s in sections: + conf_sec = getattr(CONF, s, None) + if conf_sec is not None: + if getattr(conf_sec, 'caching', True): + raise testcase.TestSkipped('%s caching enabled.' % + s) + return f(*args, **kwargs) + return inner + return wrapper + + def skip_if_no_multiple_domains_support(f): """Decorator to skip tests for identity drivers limited to one domain.""" @functools.wraps(f) @@ -223,113 +239,230 @@ class UnexpectedExit(Exception): pass -def new_ref(): - """Populates a ref with attributes common to some API entities.""" - return { +def new_region_ref(parent_region_id=None, **kwargs): + ref = { 'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex, 'description': uuid.uuid4().hex, - 'enabled': True} - + 'parent_region_id': parent_region_id} -def new_region_ref(): - ref = new_ref() - # Region doesn't have name or enabled. - del ref['name'] - del ref['enabled'] - ref['parent_region_id'] = None + ref.update(kwargs) return ref -def new_service_ref(): - ref = new_ref() - ref['type'] = uuid.uuid4().hex +def new_service_ref(**kwargs): + ref = { + 'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + 'enabled': True, + 'type': uuid.uuid4().hex, + } + ref.update(kwargs) return ref -def new_endpoint_ref(service_id, interface='public', default_region_id=None, - **kwargs): - ref = new_ref() - del ref['enabled'] # enabled is optional - ref['interface'] = interface - ref['service_id'] = service_id - ref['url'] = 'https://' + uuid.uuid4().hex + '.com' - ref['region_id'] = default_region_id +NEEDS_REGION_ID = object() + + +def new_endpoint_ref(service_id, interface='public', + region_id=NEEDS_REGION_ID, **kwargs): + + ref = { + 'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + 'interface': interface, + 'service_id': service_id, + 'url': 'https://' + uuid.uuid4().hex + '.com', + } + + if region_id is NEEDS_REGION_ID: + ref['region_id'] = uuid.uuid4().hex + elif region_id is None and kwargs.get('region') is not None: + # pre-3.2 form endpoints are not supported by this function + raise NotImplementedError("use new_endpoint_ref_with_region") + else: + ref['region_id'] = region_id ref.update(kwargs) return ref -def new_domain_ref(): - ref = new_ref() +def new_endpoint_ref_with_region(service_id, region, interface='public', + **kwargs): + """Define an endpoint_ref having a pre-3.2 form. + + Contains the deprecated 'region' instead of 'region_id'. + """ + ref = new_endpoint_ref(service_id, interface, region=region, + region_id='invalid', **kwargs) + del ref['region_id'] return ref -def new_project_ref(domain_id=None, parent_id=None, is_domain=False): - ref = new_ref() - ref['domain_id'] = domain_id - ref['parent_id'] = parent_id - ref['is_domain'] = is_domain +def new_domain_ref(**kwargs): + ref = { + 'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + 'enabled': True + } + ref.update(kwargs) + return ref + + +def new_project_ref(domain_id=None, is_domain=False, **kwargs): + ref = { + 'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + 'enabled': True, + 'domain_id': domain_id, + 'is_domain': is_domain, + } + # NOTE(henry-nash): We don't include parent_id in the initial list above + # since specifying it is optional depending on where the project sits in + # the hierarchy (and a parent_id of None has meaning - i.e. it's a top + # level project). + ref.update(kwargs) return ref -def new_user_ref(domain_id, project_id=None): - ref = new_ref() - ref['domain_id'] = domain_id - ref['email'] = uuid.uuid4().hex - ref['password'] = uuid.uuid4().hex +def new_user_ref(domain_id, project_id=None, **kwargs): + ref = { + 'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'enabled': True, + 'domain_id': domain_id, + 'email': uuid.uuid4().hex, + 'password': uuid.uuid4().hex, + } if project_id: ref['default_project_id'] = project_id + ref.update(kwargs) return ref -def new_group_ref(domain_id): - ref = new_ref() - ref['domain_id'] = domain_id +def new_federated_user_ref(idp_id=None, protocol_id=None, **kwargs): + ref = { + 'idp_id': idp_id or 'ORG_IDP', + 'protocol_id': protocol_id or 'saml2', + 'unique_id': uuid.uuid4().hex, + 'display_name': uuid.uuid4().hex, + } + ref.update(kwargs) return ref -def new_credential_ref(user_id, project_id=None, cred_type=None): - ref = dict() - ref['id'] = uuid.uuid4().hex - ref['user_id'] = user_id - if cred_type == 'ec2': - ref['type'] = 'ec2' - ref['blob'] = uuid.uuid4().hex - else: - ref['type'] = 'cert' - ref['blob'] = uuid.uuid4().hex +def new_group_ref(domain_id, **kwargs): + ref = { + 'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + 'domain_id': domain_id + } + ref.update(kwargs) + return ref + + +def new_credential_ref(user_id, project_id=None, type='cert', **kwargs): + ref = { + 'id': uuid.uuid4().hex, + 'user_id': user_id, + 'type': type, + } + if project_id: ref['project_id'] = project_id + if 'blob' not in kwargs: + ref['blob'] = uuid.uuid4().hex + + ref.update(kwargs) return ref -def new_role_ref(): - ref = new_ref() - # Roles don't have a description or the enabled flag - del ref['description'] - del ref['enabled'] +def new_cert_credential(user_id, project_id=None, blob=None, **kwargs): + if blob is None: + blob = {'access': uuid.uuid4().hex, 'secret': uuid.uuid4().hex} + + credential = new_credential_ref(user_id=user_id, + project_id=project_id, + blob=json.dumps(blob), + type='cert', + **kwargs) + return blob, credential + + +def new_ec2_credential(user_id, project_id=None, blob=None, **kwargs): + if blob is None: + blob = { + 'access': uuid.uuid4().hex, + 'secret': uuid.uuid4().hex, + 'trust_id': None + } + + if 'id' not in kwargs: + access = blob['access'].encode('utf-8') + kwargs['id'] = hashlib.sha256(access).hexdigest() + + credential = new_credential_ref(user_id=user_id, + project_id=project_id, + blob=json.dumps(blob), + type='ec2', + **kwargs) + return blob, credential + + +def new_totp_credential(user_id, project_id=None, blob=None): + if not blob: + blob = base64.b32encode(uuid.uuid4().hex).rstrip('=') + credential = new_credential_ref(user_id=user_id, + project_id=project_id, + blob=blob, + type='totp') + return credential + + +def new_role_ref(**kwargs): + ref = { + 'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'domain_id': None + } + ref.update(kwargs) return ref -def new_policy_ref(): - ref = new_ref() - ref['blob'] = uuid.uuid4().hex - ref['type'] = uuid.uuid4().hex +def new_policy_ref(**kwargs): + ref = { + 'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + 'enabled': True, + # Store serialized JSON data as the blob to mimic real world usage. + 'blob': json.dumps({'data': uuid.uuid4().hex, }), + 'type': uuid.uuid4().hex, + } + + ref.update(kwargs) return ref def new_trust_ref(trustor_user_id, trustee_user_id, project_id=None, impersonation=None, expires=None, role_ids=None, role_names=None, remaining_uses=None, - allow_redelegation=False): - ref = dict() - ref['id'] = uuid.uuid4().hex - ref['trustor_user_id'] = trustor_user_id - ref['trustee_user_id'] = trustee_user_id - ref['impersonation'] = impersonation or False - ref['project_id'] = project_id - ref['remaining_uses'] = remaining_uses - ref['allow_redelegation'] = allow_redelegation + allow_redelegation=False, redelegation_count=None, **kwargs): + ref = { + 'id': uuid.uuid4().hex, + 'trustor_user_id': trustor_user_id, + 'trustee_user_id': trustee_user_id, + 'impersonation': impersonation or False, + 'project_id': project_id, + 'remaining_uses': remaining_uses, + 'allow_redelegation': allow_redelegation, + } + + if isinstance(redelegation_count, int): + ref.update(redelegation_count=redelegation_count) if isinstance(expires, six.string_types): ref['expires_at'] = expires @@ -351,10 +484,25 @@ def new_trust_ref(trustor_user_id, trustee_user_id, project_id=None, for role_name in role_names: ref['roles'].append({'name': role_name}) + ref.update(kwargs) return ref -class BaseTestCase(oslotest.BaseTestCase): +def create_user(api, domain_id, **kwargs): + """Create a user via the API. Keep the created password. + + The password is saved and restored when api.create_user() is called. + Only use this routine if there is a requirement for the user object to + have a valid password after api.create_user() is called. + """ + user = new_user_ref(domain_id=domain_id, **kwargs) + password = user['password'] + user = api.create_user(user) + user['password'] = password + return user + + +class BaseTestCase(testtools.TestCase): """Light weight base test class. This is a placeholder that will eventually go away once the @@ -365,6 +513,10 @@ class BaseTestCase(oslotest.BaseTestCase): def setUp(self): super(BaseTestCase, self).setUp() + + self.useFixture(fixtures.NestedTempfile()) + self.useFixture(fixtures.TempHomeDir()) + self.useFixture(mockpatch.PatchObject(sys, 'exit', side_effect=UnexpectedExit)) self.useFixture(log_fixture.get_logging_handle_error_fixture()) @@ -373,6 +525,10 @@ class BaseTestCase(oslotest.BaseTestCase): module='^keystone\\.') warnings.simplefilter('error', exc.SAWarning) self.addCleanup(warnings.resetwarnings) + # Ensure we have an empty threadlocal context at the start of each + # test. + self.assertIsNone(oslo_context.get_current()) + self.useFixture(oslo_ctx_fixture.ClearRequestContext()) def cleanup_instance(self, *names): """Create a function suitable for use with self.addCleanup. @@ -395,6 +551,9 @@ class TestCase(BaseTestCase): def config_files(self): return [] + def _policy_fixture(self): + return ksfixtures.Policy(dirs.etc('policy.json'), self.config_fixture) + def config_overrides(self): # NOTE(morganfainberg): enforce config_overrides can only ever be # called a single time. @@ -403,18 +562,19 @@ class TestCase(BaseTestCase): signing_certfile = 'examples/pki/certs/signing_cert.pem' signing_keyfile = 'examples/pki/private/signing_key.pem' - self.config_fixture.config(group='oslo_policy', - policy_file=dirs.etc('policy.json')) + + self.useFixture(self._policy_fixture()) + self.config_fixture.config( # TODO(morganfainberg): Make Cache Testing a separate test case # in tempest, and move it out of the base unit tests. group='cache', backend='dogpile.cache.memory', enabled=True, - proxies=['keystone.tests.unit.test_cache.CacheIsolatingProxy']) + proxies=['oslo_cache.testing.CacheIsolatingProxy']) self.config_fixture.config( group='catalog', - driver='templated', + driver='sql', template_file=dirs.tests('default_catalog.templates')) self.config_fixture.config( group='kvs', @@ -422,7 +582,6 @@ class TestCase(BaseTestCase): ('keystone.tests.unit.test_kvs.' 'KVSBackendForcedKeyMangleFixture'), 'keystone.tests.unit.test_kvs.KVSBackendFixture']) - self.config_fixture.config(group='revoke', driver='kvs') self.config_fixture.config( group='signing', certfile=signing_certfile, keyfile=signing_keyfile, @@ -444,17 +603,15 @@ class TestCase(BaseTestCase): 'routes.middleware=INFO', 'stevedore.extension=INFO', 'keystone.notifications=INFO', - 'keystone.common._memcache_pool=INFO', 'keystone.common.ldap=INFO', ]) self.auth_plugin_config_override() def auth_plugin_config_override(self, methods=None, **method_classes): - if methods is not None: - self.config_fixture.config(group='auth', methods=methods) - common_cfg.setup_authentication() - if method_classes: - self.config_fixture.config(group='auth', **method_classes) + self.useFixture( + ksfixtures.ConfigAuthPlugins(self.config_fixture, + methods, + **method_classes)) def _assert_config_overrides_called(self): assert self.__config_overrides_called is True @@ -462,6 +619,7 @@ class TestCase(BaseTestCase): def setUp(self): super(TestCase, self).setUp() self.__config_overrides_called = False + self.__load_backends_called = False self.addCleanup(CONF.reset) self.config_fixture = self.useFixture(config_fixture.Config(CONF)) self.addCleanup(delattr, self, 'config_fixture') @@ -473,9 +631,10 @@ class TestCase(BaseTestCase): def mocked_register_auth_plugin_opt(conf, opt): self.config_fixture.register_opt(opt, group='auth') self.useFixture(mockpatch.PatchObject( - common_cfg, '_register_auth_plugin_opt', + config, '_register_auth_plugin_opt', new=mocked_register_auth_plugin_opt)) + self.sql_driver_version_overrides = {} self.config_overrides() # NOTE(morganfainberg): ensure config_overrides has been called. self.addCleanup(self._assert_config_overrides_called) @@ -498,8 +657,6 @@ class TestCase(BaseTestCase): # tests aren't used. self.addCleanup(dependency.reset) - self.addCleanup(kvs.INMEMDB.clear) - # Ensure Notification subscriptions and resource types are empty self.addCleanup(notifications.clear_subscribers) self.addCleanup(notifications.reset_notifier) @@ -515,7 +672,6 @@ class TestCase(BaseTestCase): def load_backends(self): """Initializes each manager and assigns them to an attribute.""" - # TODO(blk-u): Shouldn't need to clear the registry here, but some # tests call load_backends multiple times. These should be fixed to # only call load_backends once. @@ -541,7 +697,7 @@ class TestCase(BaseTestCase): This is useful to load managers initialized by extensions. No extra backends are loaded by default. - :return: dict of name -> manager + :returns: dict of name -> manager """ return {} @@ -573,7 +729,8 @@ class TestCase(BaseTestCase): fixtures_to_cleanup.append(attrname) for tenant in fixtures.TENANTS: - if hasattr(self, 'tenant_%s' % tenant['id']): + tenant_attr_name = 'tenant_%s' % tenant['name'].lower() + if hasattr(self, tenant_attr_name): try: # This will clear out any roles on the project as well self.resource_api.delete_project(tenant['id']) @@ -582,9 +739,8 @@ class TestCase(BaseTestCase): rv = self.resource_api.create_project( tenant['id'], tenant) - attrname = 'tenant_%s' % tenant['id'] - setattr(self, attrname, rv) - fixtures_to_cleanup.append(attrname) + setattr(self, tenant_attr_name, rv) + fixtures_to_cleanup.append(tenant_attr_name) for role in fixtures.ROLES: try: @@ -625,6 +781,17 @@ class TestCase(BaseTestCase): setattr(self, attrname, user_copy) fixtures_to_cleanup.append(attrname) + for role_assignment in fixtures.ROLE_ASSIGNMENTS: + role_id = role_assignment['role_id'] + user = role_assignment['user'] + tenant_id = role_assignment['tenant_id'] + user_id = getattr(self, 'user_%s' % user)['id'] + try: + self.assignment_api.add_role_to_user_and_project( + user_id, tenant_id, role_id) + except exception.Conflict: + pass + self.addCleanup(self.cleanup_instance(*fixtures_to_cleanup)) def _paste_config(self, config): @@ -648,6 +815,10 @@ class TestCase(BaseTestCase): :param delta: Maximum allowable time delta, defined in seconds. """ + if a == b: + # Short-circuit if the values are the same. + return + msg = '%s != %s within %s delta' % (a, b, delta) self.assertTrue(abs(a - b).seconds <= delta, msg) @@ -664,11 +835,11 @@ class TestCase(BaseTestCase): if isinstance(expected_regexp, six.string_types): expected_regexp = re.compile(expected_regexp) - if isinstance(exc_value.args[0], unicode): - if not expected_regexp.search(unicode(exc_value)): + if isinstance(exc_value.args[0], six.text_type): + if not expected_regexp.search(six.text_type(exc_value)): raise self.failureException( '"%s" does not match "%s"' % - (expected_regexp.pattern, unicode(exc_value))) + (expected_regexp.pattern, six.text_type(exc_value))) else: if not expected_regexp.search(str(exc_value)): raise self.failureException( @@ -708,12 +879,29 @@ class TestCase(BaseTestCase): class SQLDriverOverrides(object): """A mixin for consolidating sql-specific test overrides.""" + def config_overrides(self): super(SQLDriverOverrides, self).config_overrides() # SQL specific driver overrides self.config_fixture.config(group='catalog', driver='sql') self.config_fixture.config(group='identity', driver='sql') self.config_fixture.config(group='policy', driver='sql') - self.config_fixture.config(group='revoke', driver='sql') self.config_fixture.config(group='token', driver='sql') self.config_fixture.config(group='trust', driver='sql') + + def use_specific_sql_driver_version(self, driver_path, + versionless_backend, version_suffix): + """Add this versioned driver to the list that will be loaded. + + :param driver_path: The path to the drivers, e.g. 'keystone.assignment' + :param versionless_backend: The name of the versionless drivers, e.g. + 'backends' + :param version_suffix: The suffix for the version , e.g. ``V8_`` + + This method assumes that versioned drivers are named: + , e.g. 'V8_backends'. + + """ + self.sql_driver_version_overrides[driver_path] = { + 'versionless_backend': versionless_backend, + 'versioned_backend': version_suffix + versionless_backend} diff --git a/keystone-moon/keystone/tests/unit/default_fixtures.py b/keystone-moon/keystone/tests/unit/default_fixtures.py index 80b0665f..7f661986 100644 --- a/keystone-moon/keystone/tests/unit/default_fixtures.py +++ b/keystone-moon/keystone/tests/unit/default_fixtures.py @@ -14,53 +14,67 @@ # NOTE(dolph): please try to avoid additional fixtures if possible; test suite # performance may be negatively affected. +import uuid +BAR_TENANT_ID = uuid.uuid4().hex +BAZ_TENANT_ID = uuid.uuid4().hex +MTU_TENANT_ID = uuid.uuid4().hex +SERVICE_TENANT_ID = uuid.uuid4().hex DEFAULT_DOMAIN_ID = 'default' TENANTS = [ { - 'id': 'bar', + 'id': BAR_TENANT_ID, 'name': 'BAR', 'domain_id': DEFAULT_DOMAIN_ID, 'description': 'description', 'enabled': True, - 'parent_id': None, + 'parent_id': DEFAULT_DOMAIN_ID, 'is_domain': False, }, { - 'id': 'baz', + 'id': BAZ_TENANT_ID, 'name': 'BAZ', 'domain_id': DEFAULT_DOMAIN_ID, 'description': 'description', 'enabled': True, - 'parent_id': None, + 'parent_id': DEFAULT_DOMAIN_ID, 'is_domain': False, }, { - 'id': 'mtu', + 'id': MTU_TENANT_ID, 'name': 'MTU', 'description': 'description', 'enabled': True, 'domain_id': DEFAULT_DOMAIN_ID, - 'parent_id': None, + 'parent_id': DEFAULT_DOMAIN_ID, 'is_domain': False, }, { - 'id': 'service', + 'id': SERVICE_TENANT_ID, 'name': 'service', 'description': 'description', 'enabled': True, 'domain_id': DEFAULT_DOMAIN_ID, - 'parent_id': None, + 'parent_id': DEFAULT_DOMAIN_ID, 'is_domain': False, } ] # NOTE(ja): a role of keystone_admin is done in setUp USERS = [ + # NOTE(morganfainberg): Admin user for replacing admin_token_auth + { + 'id': 'reqadmin', + 'name': 'REQ_ADMIN', + 'domain_id': DEFAULT_DOMAIN_ID, + 'password': 'password', + 'tenants': [], + 'enabled': True + }, { 'id': 'foo', 'name': 'FOO', 'domain_id': DEFAULT_DOMAIN_ID, 'password': 'foo2', - 'tenants': ['bar'], + 'tenants': [BAR_TENANT_ID], 'enabled': True, 'email': 'foo@bar.com', }, { @@ -69,8 +83,8 @@ USERS = [ 'domain_id': DEFAULT_DOMAIN_ID, 'password': 'two2', 'enabled': True, - 'default_project_id': 'baz', - 'tenants': ['baz'], + 'default_project_id': BAZ_TENANT_ID, + 'tenants': [BAZ_TENANT_ID], 'email': 'two@three.com', }, { 'id': 'badguy', @@ -78,8 +92,8 @@ USERS = [ 'domain_id': DEFAULT_DOMAIN_ID, 'password': 'bad', 'enabled': False, - 'default_project_id': 'baz', - 'tenants': ['baz'], + 'default_project_id': BAZ_TENANT_ID, + 'tenants': [BAZ_TENANT_ID], 'email': 'bad@guy.com', }, { 'id': 'sna', @@ -87,7 +101,7 @@ USERS = [ 'domain_id': DEFAULT_DOMAIN_ID, 'password': 'snafu', 'enabled': True, - 'tenants': ['bar'], + 'tenants': [BAR_TENANT_ID], 'email': 'sna@snl.coom', } ] @@ -96,30 +110,45 @@ ROLES = [ { 'id': 'admin', 'name': 'admin', + 'domain_id': None, }, { 'id': 'member', 'name': 'Member', + 'domain_id': None, }, { 'id': '9fe2ff9ee4384b1894a90878d3e92bab', 'name': '_member_', + 'domain_id': None, }, { 'id': 'other', 'name': 'Other', + 'domain_id': None, }, { 'id': 'browser', 'name': 'Browser', + 'domain_id': None, }, { 'id': 'writer', 'name': 'Writer', + 'domain_id': None, }, { 'id': 'service', 'name': 'Service', + 'domain_id': None, } ] +# NOTE(morganfainberg): Admin assignment for replacing admin_token_auth +ROLE_ASSIGNMENTS = [ + { + 'user': 'reqadmin', + 'tenant_id': SERVICE_TENANT_ID, + 'role_id': 'admin' + }, +] + DOMAINS = [{'description': - (u'Owns users and tenants (i.e. projects)' - ' available on Identity API v2.'), + (u'The default domain'), 'enabled': True, 'id': DEFAULT_DOMAIN_ID, 'name': u'Default'}] diff --git a/keystone-moon/keystone/tests/unit/external/README.rst b/keystone-moon/keystone/tests/unit/external/README.rst new file mode 100644 index 00000000..e8f9fa65 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/external/README.rst @@ -0,0 +1,9 @@ +This directory contains interface tests for external libraries. The goal +is not to test every possible path through a library's code and get 100% +coverage. It's to give us a level of confidence that their general interface +remains the same through version upgrades. + +This gives us a place to put these tests without having to litter our +own tests with assertions that are not directly related to the code +under test. The expectations for the external library are all in one +place so it makes it easier for us to find out what they are. diff --git a/keystone-moon/keystone/tests/unit/external/__init__.py b/keystone-moon/keystone/tests/unit/external/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/unit/external/test_timeutils.py b/keystone-moon/keystone/tests/unit/external/test_timeutils.py new file mode 100644 index 00000000..7fc72d58 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/external/test_timeutils.py @@ -0,0 +1,33 @@ +# 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 datetime + +from oslo_utils import timeutils + +import keystone.tests.unit as tests + + +class TestTimeUtils(tests.BaseTestCase): + + def test_parsing_date_strings_returns_a_datetime(self): + example_date_str = '2015-09-23T04:45:37.196621Z' + dt = timeutils.parse_strtime(example_date_str, fmt=tests.TIME_FORMAT) + self.assertIsInstance(dt, datetime.datetime) + + def test_parsing_invalid_date_strings_raises_a_ValueError(self): + example_date_str = '' + simple_format = '%Y' + self.assertRaises(ValueError, + timeutils.parse_strtime, + example_date_str, + fmt=simple_format) diff --git a/keystone-moon/keystone/tests/unit/fakeldap.py b/keystone-moon/keystone/tests/unit/fakeldap.py index 2f1ebe57..9ad1f218 100644 --- a/keystone-moon/keystone/tests/unit/fakeldap.py +++ b/keystone-moon/keystone/tests/unit/fakeldap.py @@ -18,10 +18,11 @@ 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. +library to work with keystone. """ +import random import re import shelve @@ -67,7 +68,13 @@ def _internal_attr(attr_name, value_or_values): if dn == 'cn=Doe\\, John,ou=Users,cn=example,cn=com': return 'CN=Doe\\2C John,OU=Users,CN=example,CN=com' - dn = ldap.dn.str2dn(core.utf8_encode(dn)) + try: + dn = ldap.dn.str2dn(core.utf8_encode(dn)) + except ldap.DECODING_ERROR: + # NOTE(amakarov): In case of IDs instead of DNs in group members + # they must be handled as regular values. + return normalize_value(dn) + norm = [] for part in dn: name, val, i = part[0] @@ -132,7 +139,6 @@ def _paren_groups(source): def _match(key, value, attrs): """Match a given key and value against an attribute list.""" - def match_with_wildcards(norm_val, val_list): # Case insensitive checking with wildcards if norm_val.startswith('*'): @@ -209,6 +215,7 @@ class FakeShelve(dict): FakeShelves = {} +PendingRequests = {} class FakeLdap(core.LDAPHandler): @@ -534,18 +541,60 @@ class FakeLdap(core.LDAPHandler): self._ldap_options[option] = invalue def get_option(self, option): - value = self._ldap_options.get(option, None) + value = self._ldap_options.get(option) return value def search_ext(self, base, scope, filterstr='(objectClass=*)', attrlist=None, attrsonly=0, serverctrls=None, clientctrls=None, timeout=-1, sizelimit=0): - raise exception.NotImplemented() + if clientctrls is not None or timeout != -1 or sizelimit != 0: + raise exception.NotImplemented() + + # only passing a single server control is supported by this fake ldap + if len(serverctrls) > 1: + raise exception.NotImplemented() + + # search_ext is async and returns an identifier used for + # retrieving the results via result3(). This will be emulated by + # storing the request in a variable with random integer key and + # performing the real lookup in result3() + msgid = random.randint(0, 1000) + PendingRequests[msgid] = (base, scope, filterstr, attrlist, attrsonly, + serverctrls) + return msgid def result3(self, msgid=ldap.RES_ANY, all=1, timeout=None, resp_ctrl_classes=None): - raise exception.NotImplemented() + """Execute async request + + Only msgid param is supported. Request info is fetched from global + variable `PendingRequests` by msgid, executed using search_s and + limited if requested. + """ + if all != 1 or timeout is not None or resp_ctrl_classes is not None: + raise exception.NotImplemented() + + params = PendingRequests[msgid] + # search_s accepts a subset of parameters of search_ext, + # that's why we use only the first 5. + results = self.search_s(*params[:5]) + + # extract limit from serverctrl + serverctrls = params[5] + ctrl = serverctrls[0] + + if ctrl.size: + rdata = results[:ctrl.size] + else: + rdata = results + + # real result3 returns various service info -- rtype, rmsgid, + # serverctrls. Now this info is not used, so all this info is None + rtype = None + rmsgid = None + serverctrls = None + return (rtype, rdata, rmsgid, serverctrls) class FakeLdapPool(FakeLdap): diff --git a/keystone-moon/keystone/tests/unit/filtering.py b/keystone-moon/keystone/tests/unit/filtering.py index 93e0bc28..59301299 100644 --- a/keystone-moon/keystone/tests/unit/filtering.py +++ b/keystone-moon/keystone/tests/unit/filtering.py @@ -49,7 +49,6 @@ class FilterTests(object): one. """ - f = getattr(self.identity_api, 'create_%s' % entity_type, None) if f is None: f = getattr(self.resource_api, 'create_%s' % entity_type, None) @@ -65,7 +64,6 @@ class FilterTests(object): one. """ - f = getattr(self.identity_api, 'delete_%s' % entity_type, None) if f is None: f = getattr(self.resource_api, 'delete_%s' % entity_type, None) @@ -81,7 +79,6 @@ class FilterTests(object): one. """ - f = getattr(self.identity_api, 'list_%ss' % entity_type, None) if f is None: f = getattr(self.resource_api, 'list_%ss' % entity_type, None) diff --git a/keystone-moon/keystone/tests/unit/identity/test_backends.py b/keystone-moon/keystone/tests/unit/identity/test_backends.py new file mode 100644 index 00000000..8b5c0def --- /dev/null +++ b/keystone-moon/keystone/tests/unit/identity/test_backends.py @@ -0,0 +1,1297 @@ +# Copyright 2012 OpenStack Foundation +# +# 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 + +import mock +from oslo_config import cfg +from six.moves import range +from testtools import matchers + +from keystone.common import driver_hints +from keystone import exception +from keystone.tests import unit +from keystone.tests.unit import default_fixtures +from keystone.tests.unit import filtering + + +CONF = cfg.CONF + + +class IdentityTests(object): + + def _get_domain_fixture(self): + domain = unit.new_domain_ref() + self.resource_api.create_domain(domain['id'], domain) + return domain + + def _set_domain_scope(self, domain_id): + # We only provide a domain scope if we have multiple drivers + if CONF.identity.domain_specific_drivers_enabled: + return domain_id + + def test_authenticate_bad_user(self): + self.assertRaises(AssertionError, + self.identity_api.authenticate, + context={}, + user_id=uuid.uuid4().hex, + password=self.user_foo['password']) + + def test_authenticate_bad_password(self): + self.assertRaises(AssertionError, + self.identity_api.authenticate, + context={}, + user_id=self.user_foo['id'], + password=uuid.uuid4().hex) + + def test_authenticate(self): + user_ref = self.identity_api.authenticate( + context={}, + user_id=self.user_sna['id'], + password=self.user_sna['password']) + # NOTE(termie): the password field is left in user_sna to make + # it easier to authenticate in tests, but should + # not be returned by the api + self.user_sna.pop('password') + self.user_sna['enabled'] = True + self.assertDictEqual(self.user_sna, user_ref) + + def test_authenticate_and_get_roles_no_metadata(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + + # Remove user id. It is ignored by create_user() and will break the + # subset test below. + del user['id'] + + new_user = self.identity_api.create_user(user) + self.assignment_api.add_user_to_project(self.tenant_baz['id'], + new_user['id']) + user_ref = self.identity_api.authenticate( + context={}, + user_id=new_user['id'], + password=user['password']) + self.assertNotIn('password', user_ref) + # NOTE(termie): the password field is left in user_sna to make + # it easier to authenticate in tests, but should + # not be returned by the api + user.pop('password') + self.assertDictContainsSubset(user, user_ref) + role_list = self.assignment_api.get_roles_for_user_and_project( + new_user['id'], self.tenant_baz['id']) + self.assertEqual(1, len(role_list)) + self.assertIn(CONF.member_role_id, role_list) + + def test_authenticate_if_no_password_set(self): + id_ = uuid.uuid4().hex + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + self.identity_api.create_user(user) + + self.assertRaises(AssertionError, + self.identity_api.authenticate, + context={}, + user_id=id_, + password='password') + + def test_create_unicode_user_name(self): + unicode_name = u'name \u540d\u5b57' + user = unit.new_user_ref(name=unicode_name, + domain_id=CONF.identity.default_domain_id) + ref = self.identity_api.create_user(user) + self.assertEqual(unicode_name, ref['name']) + + def test_get_user(self): + user_ref = self.identity_api.get_user(self.user_foo['id']) + # NOTE(termie): the password field is left in user_foo to make + # it easier to authenticate in tests, but should + # not be returned by the api + self.user_foo.pop('password') + self.assertDictEqual(self.user_foo, user_ref) + + @unit.skip_if_cache_disabled('identity') + def test_cache_layer_get_user(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + self.identity_api.create_user(user) + ref = self.identity_api.get_user_by_name(user['name'], + user['domain_id']) + # cache the result. + self.identity_api.get_user(ref['id']) + # delete bypassing identity api + domain_id, driver, entity_id = ( + self.identity_api._get_domain_driver_and_entity_id(ref['id'])) + driver.delete_user(entity_id) + + self.assertDictEqual(ref, self.identity_api.get_user(ref['id'])) + self.identity_api.get_user.invalidate(self.identity_api, ref['id']) + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user, ref['id']) + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user = self.identity_api.create_user(user) + ref = self.identity_api.get_user_by_name(user['name'], + user['domain_id']) + user['description'] = uuid.uuid4().hex + # cache the result. + self.identity_api.get_user(ref['id']) + # update using identity api and get back updated user. + user_updated = self.identity_api.update_user(ref['id'], user) + self.assertDictContainsSubset(self.identity_api.get_user(ref['id']), + user_updated) + self.assertDictContainsSubset( + self.identity_api.get_user_by_name(ref['name'], ref['domain_id']), + user_updated) + + def test_get_user_returns_not_found(self): + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user, + uuid.uuid4().hex) + + def test_get_user_by_name(self): + user_ref = self.identity_api.get_user_by_name( + self.user_foo['name'], CONF.identity.default_domain_id) + # NOTE(termie): the password field is left in user_foo to make + # it easier to authenticate in tests, but should + # not be returned by the api + self.user_foo.pop('password') + self.assertDictEqual(self.user_foo, user_ref) + + @unit.skip_if_cache_disabled('identity') + def test_cache_layer_get_user_by_name(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + self.identity_api.create_user(user) + ref = self.identity_api.get_user_by_name(user['name'], + user['domain_id']) + # delete bypassing the identity api. + domain_id, driver, entity_id = ( + self.identity_api._get_domain_driver_and_entity_id(ref['id'])) + driver.delete_user(entity_id) + + self.assertDictEqual(ref, self.identity_api.get_user_by_name( + user['name'], CONF.identity.default_domain_id)) + self.identity_api.get_user_by_name.invalidate( + self.identity_api, user['name'], CONF.identity.default_domain_id) + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user_by_name, + user['name'], CONF.identity.default_domain_id) + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user = self.identity_api.create_user(user) + ref = self.identity_api.get_user_by_name(user['name'], + user['domain_id']) + user['description'] = uuid.uuid4().hex + user_updated = self.identity_api.update_user(ref['id'], user) + self.assertDictContainsSubset(self.identity_api.get_user(ref['id']), + user_updated) + self.assertDictContainsSubset( + self.identity_api.get_user_by_name(ref['name'], ref['domain_id']), + user_updated) + + def test_get_user_by_name_returns_not_found(self): + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user_by_name, + uuid.uuid4().hex, + CONF.identity.default_domain_id) + + def test_create_duplicate_user_name_fails(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user = self.identity_api.create_user(user) + self.assertRaises(exception.Conflict, + self.identity_api.create_user, + user) + + def test_create_duplicate_user_name_in_different_domains(self): + new_domain = unit.new_domain_ref() + self.resource_api.create_domain(new_domain['id'], new_domain) + user1 = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + + user2 = unit.new_user_ref(name=user1['name'], + domain_id=new_domain['id']) + + self.identity_api.create_user(user1) + self.identity_api.create_user(user2) + + def test_move_user_between_domains(self): + domain1 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = unit.new_domain_ref() + self.resource_api.create_domain(domain2['id'], domain2) + user = unit.new_user_ref(domain_id=domain1['id']) + user = self.identity_api.create_user(user) + user['domain_id'] = domain2['id'] + # Update the user asserting that a deprecation warning is emitted + with mock.patch( + 'oslo_log.versionutils.report_deprecated_feature') as mock_dep: + self.identity_api.update_user(user['id'], user) + self.assertTrue(mock_dep.called) + + updated_user_ref = self.identity_api.get_user(user['id']) + self.assertEqual(domain2['id'], updated_user_ref['domain_id']) + + def test_move_user_between_domains_with_clashing_names_fails(self): + domain1 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = unit.new_domain_ref() + self.resource_api.create_domain(domain2['id'], domain2) + # First, create a user in domain1 + user1 = unit.new_user_ref(domain_id=domain1['id']) + user1 = self.identity_api.create_user(user1) + # Now create a user in domain2 with a potentially clashing + # name - which should work since we have domain separation + user2 = unit.new_user_ref(name=user1['name'], + domain_id=domain2['id']) + user2 = self.identity_api.create_user(user2) + # Now try and move user1 into the 2nd domain - which should + # fail since the names clash + user1['domain_id'] = domain2['id'] + self.assertRaises(exception.Conflict, + self.identity_api.update_user, + user1['id'], + user1) + + def test_rename_duplicate_user_name_fails(self): + user1 = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user2 = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + self.identity_api.create_user(user1) + user2 = self.identity_api.create_user(user2) + user2['name'] = user1['name'] + self.assertRaises(exception.Conflict, + self.identity_api.update_user, + user2['id'], + user2) + + def test_update_user_id_fails(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user = self.identity_api.create_user(user) + original_id = user['id'] + user['id'] = 'fake2' + self.assertRaises(exception.ValidationError, + self.identity_api.update_user, + original_id, + user) + user_ref = self.identity_api.get_user(original_id) + self.assertEqual(original_id, user_ref['id']) + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user, + 'fake2') + + def test_delete_user_with_group_project_domain_links(self): + role1 = unit.new_role_ref() + self.role_api.create_role(role1['id'], role1) + domain1 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + project1 = unit.new_project_ref(domain_id=domain1['id']) + self.resource_api.create_project(project1['id'], project1) + user1 = unit.new_user_ref(domain_id=domain1['id']) + user1 = self.identity_api.create_user(user1) + group1 = unit.new_group_ref(domain_id=domain1['id']) + group1 = self.identity_api.create_group(group1) + self.assignment_api.create_grant(user_id=user1['id'], + project_id=project1['id'], + role_id=role1['id']) + self.assignment_api.create_grant(user_id=user1['id'], + domain_id=domain1['id'], + role_id=role1['id']) + self.identity_api.add_user_to_group(user_id=user1['id'], + group_id=group1['id']) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + project_id=project1['id']) + self.assertEqual(1, len(roles_ref)) + roles_ref = self.assignment_api.list_grants( + user_id=user1['id'], + domain_id=domain1['id']) + self.assertEqual(1, len(roles_ref)) + self.identity_api.check_user_in_group( + user_id=user1['id'], + group_id=group1['id']) + self.identity_api.delete_user(user1['id']) + self.assertRaises(exception.NotFound, + self.identity_api.check_user_in_group, + user1['id'], + group1['id']) + + def test_delete_group_with_user_project_domain_links(self): + role1 = unit.new_role_ref() + self.role_api.create_role(role1['id'], role1) + domain1 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + project1 = unit.new_project_ref(domain_id=domain1['id']) + self.resource_api.create_project(project1['id'], project1) + user1 = unit.new_user_ref(domain_id=domain1['id']) + user1 = self.identity_api.create_user(user1) + group1 = unit.new_group_ref(domain_id=domain1['id']) + group1 = self.identity_api.create_group(group1) + + self.assignment_api.create_grant(group_id=group1['id'], + project_id=project1['id'], + role_id=role1['id']) + self.assignment_api.create_grant(group_id=group1['id'], + domain_id=domain1['id'], + role_id=role1['id']) + self.identity_api.add_user_to_group(user_id=user1['id'], + group_id=group1['id']) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + project_id=project1['id']) + self.assertEqual(1, len(roles_ref)) + roles_ref = self.assignment_api.list_grants( + group_id=group1['id'], + domain_id=domain1['id']) + self.assertEqual(1, len(roles_ref)) + self.identity_api.check_user_in_group( + user_id=user1['id'], + group_id=group1['id']) + self.identity_api.delete_group(group1['id']) + self.identity_api.get_user(user1['id']) + + def test_update_user_returns_not_found(self): + user_id = uuid.uuid4().hex + self.assertRaises(exception.UserNotFound, + self.identity_api.update_user, + user_id, + {'id': user_id, + 'domain_id': CONF.identity.default_domain_id}) + + def test_delete_user_returns_not_found(self): + self.assertRaises(exception.UserNotFound, + self.identity_api.delete_user, + uuid.uuid4().hex) + + def test_create_user_long_name_fails(self): + user = unit.new_user_ref(name='a' * 256, + domain_id=CONF.identity.default_domain_id) + self.assertRaises(exception.ValidationError, + self.identity_api.create_user, + user) + + def test_create_user_blank_name_fails(self): + user = unit.new_user_ref(name='', + domain_id=CONF.identity.default_domain_id) + self.assertRaises(exception.ValidationError, + self.identity_api.create_user, + user) + + def test_create_user_missed_password(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user = self.identity_api.create_user(user) + self.identity_api.get_user(user['id']) + # Make sure the user is not allowed to login + # with a password that is empty string or None + self.assertRaises(AssertionError, + self.identity_api.authenticate, + context={}, + user_id=user['id'], + password='') + self.assertRaises(AssertionError, + self.identity_api.authenticate, + context={}, + user_id=user['id'], + password=None) + + def test_create_user_none_password(self): + user = unit.new_user_ref(password=None, + domain_id=CONF.identity.default_domain_id) + user = self.identity_api.create_user(user) + self.identity_api.get_user(user['id']) + # Make sure the user is not allowed to login + # with a password that is empty string or None + self.assertRaises(AssertionError, + self.identity_api.authenticate, + context={}, + user_id=user['id'], + password='') + self.assertRaises(AssertionError, + self.identity_api.authenticate, + context={}, + user_id=user['id'], + password=None) + + def test_create_user_invalid_name_fails(self): + user = unit.new_user_ref(name=None, + domain_id=CONF.identity.default_domain_id) + self.assertRaises(exception.ValidationError, + self.identity_api.create_user, + user) + + user = unit.new_user_ref(name=123, + domain_id=CONF.identity.default_domain_id) + self.assertRaises(exception.ValidationError, + self.identity_api.create_user, + user) + + def test_create_user_invalid_enabled_type_string(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id, + # invalid string value + enabled='true') + self.assertRaises(exception.ValidationError, + self.identity_api.create_user, + user) + + def test_update_user_long_name_fails(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user = self.identity_api.create_user(user) + user['name'] = 'a' * 256 + self.assertRaises(exception.ValidationError, + self.identity_api.update_user, + user['id'], + user) + + def test_update_user_blank_name_fails(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user = self.identity_api.create_user(user) + user['name'] = '' + self.assertRaises(exception.ValidationError, + self.identity_api.update_user, + user['id'], + user) + + def test_update_user_invalid_name_fails(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user = self.identity_api.create_user(user) + + user['name'] = None + self.assertRaises(exception.ValidationError, + self.identity_api.update_user, + user['id'], + user) + + user['name'] = 123 + self.assertRaises(exception.ValidationError, + self.identity_api.update_user, + user['id'], + user) + + def test_list_users(self): + users = self.identity_api.list_users( + domain_scope=self._set_domain_scope( + CONF.identity.default_domain_id)) + self.assertEqual(len(default_fixtures.USERS), len(users)) + user_ids = set(user['id'] for user in users) + expected_user_ids = set(getattr(self, 'user_%s' % user['id'])['id'] + for user in default_fixtures.USERS) + for user_ref in users: + self.assertNotIn('password', user_ref) + self.assertEqual(expected_user_ids, user_ids) + + def test_list_groups(self): + group1 = unit.new_group_ref(domain_id=CONF.identity.default_domain_id) + group2 = unit.new_group_ref(domain_id=CONF.identity.default_domain_id) + group1 = self.identity_api.create_group(group1) + group2 = self.identity_api.create_group(group2) + groups = self.identity_api.list_groups( + domain_scope=self._set_domain_scope( + CONF.identity.default_domain_id)) + self.assertEqual(2, len(groups)) + group_ids = [] + for group in groups: + group_ids.append(group.get('id')) + self.assertIn(group1['id'], group_ids) + self.assertIn(group2['id'], group_ids) + + def test_create_user_doesnt_modify_passed_in_dict(self): + new_user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + original_user = new_user.copy() + self.identity_api.create_user(new_user) + self.assertDictEqual(original_user, new_user) + + def test_update_user_enable(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user = self.identity_api.create_user(user) + user_ref = self.identity_api.get_user(user['id']) + self.assertTrue(user_ref['enabled']) + + user['enabled'] = False + self.identity_api.update_user(user['id'], user) + user_ref = self.identity_api.get_user(user['id']) + self.assertEqual(user['enabled'], user_ref['enabled']) + + # If not present, enabled field should not be updated + del user['enabled'] + self.identity_api.update_user(user['id'], user) + user_ref = self.identity_api.get_user(user['id']) + self.assertFalse(user_ref['enabled']) + + user['enabled'] = True + self.identity_api.update_user(user['id'], user) + user_ref = self.identity_api.get_user(user['id']) + self.assertEqual(user['enabled'], user_ref['enabled']) + + del user['enabled'] + self.identity_api.update_user(user['id'], user) + user_ref = self.identity_api.get_user(user['id']) + self.assertTrue(user_ref['enabled']) + + # Integers are valid Python's booleans. Explicitly test it. + user['enabled'] = 0 + self.identity_api.update_user(user['id'], user) + user_ref = self.identity_api.get_user(user['id']) + self.assertFalse(user_ref['enabled']) + + # Any integers other than 0 are interpreted as True + user['enabled'] = -42 + self.identity_api.update_user(user['id'], user) + user_ref = self.identity_api.get_user(user['id']) + # NOTE(breton): below, attribute `enabled` is explicitly tested to be + # equal True. assertTrue should not be used, because it converts + # the passed value to bool(). + self.assertIs(user_ref['enabled'], True) + + def test_update_user_name(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user = self.identity_api.create_user(user) + user_ref = self.identity_api.get_user(user['id']) + self.assertEqual(user['name'], user_ref['name']) + + changed_name = user_ref['name'] + '_changed' + user_ref['name'] = changed_name + updated_user = self.identity_api.update_user(user_ref['id'], user_ref) + + # NOTE(dstanek): the SQL backend adds an 'extra' field containing a + # dictionary of the extra fields in addition to the + # fields in the object. For the details see: + # SqlIdentity.test_update_project_returns_extra + updated_user.pop('extra', None) + + self.assertDictEqual(user_ref, updated_user) + + user_ref = self.identity_api.get_user(user_ref['id']) + self.assertEqual(changed_name, user_ref['name']) + + def test_update_user_enable_fails(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user = self.identity_api.create_user(user) + user_ref = self.identity_api.get_user(user['id']) + self.assertTrue(user_ref['enabled']) + + # Strings are not valid boolean values + user['enabled'] = 'false' + self.assertRaises(exception.ValidationError, + self.identity_api.update_user, + user['id'], + user) + + def test_add_user_to_group(self): + domain = self._get_domain_fixture() + new_group = unit.new_group_ref(domain_id=domain['id']) + new_group = self.identity_api.create_group(new_group) + new_user = unit.new_user_ref(domain_id=domain['id']) + new_user = self.identity_api.create_user(new_user) + self.identity_api.add_user_to_group(new_user['id'], + new_group['id']) + groups = self.identity_api.list_groups_for_user(new_user['id']) + + found = False + for x in groups: + if (x['id'] == new_group['id']): + found = True + self.assertTrue(found) + + def test_add_user_to_group_returns_not_found(self): + domain = self._get_domain_fixture() + new_user = unit.new_user_ref(domain_id=domain['id']) + new_user = self.identity_api.create_user(new_user) + self.assertRaises(exception.GroupNotFound, + self.identity_api.add_user_to_group, + new_user['id'], + uuid.uuid4().hex) + + new_group = unit.new_group_ref(domain_id=domain['id']) + new_group = self.identity_api.create_group(new_group) + self.assertRaises(exception.UserNotFound, + self.identity_api.add_user_to_group, + uuid.uuid4().hex, + new_group['id']) + + self.assertRaises(exception.NotFound, + self.identity_api.add_user_to_group, + uuid.uuid4().hex, + uuid.uuid4().hex) + + def test_check_user_in_group(self): + domain = self._get_domain_fixture() + new_group = unit.new_group_ref(domain_id=domain['id']) + new_group = self.identity_api.create_group(new_group) + new_user = unit.new_user_ref(domain_id=domain['id']) + new_user = self.identity_api.create_user(new_user) + self.identity_api.add_user_to_group(new_user['id'], + new_group['id']) + self.identity_api.check_user_in_group(new_user['id'], new_group['id']) + + def test_check_user_not_in_group(self): + new_group = unit.new_group_ref( + domain_id=CONF.identity.default_domain_id) + new_group = self.identity_api.create_group(new_group) + + new_user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + new_user = self.identity_api.create_user(new_user) + + self.assertRaises(exception.NotFound, + self.identity_api.check_user_in_group, + new_user['id'], + new_group['id']) + + def test_check_user_in_group_returns_not_found(self): + new_user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + new_user = self.identity_api.create_user(new_user) + + new_group = unit.new_group_ref( + domain_id=CONF.identity.default_domain_id) + new_group = self.identity_api.create_group(new_group) + + self.assertRaises(exception.UserNotFound, + self.identity_api.check_user_in_group, + uuid.uuid4().hex, + new_group['id']) + + self.assertRaises(exception.GroupNotFound, + self.identity_api.check_user_in_group, + new_user['id'], + uuid.uuid4().hex) + + self.assertRaises(exception.NotFound, + self.identity_api.check_user_in_group, + uuid.uuid4().hex, + uuid.uuid4().hex) + + def test_list_users_in_group(self): + domain = self._get_domain_fixture() + new_group = unit.new_group_ref(domain_id=domain['id']) + new_group = self.identity_api.create_group(new_group) + # Make sure we get an empty list back on a new group, not an error. + user_refs = self.identity_api.list_users_in_group(new_group['id']) + self.assertEqual([], user_refs) + # Make sure we get the correct users back once they have been added + # to the group. + new_user = unit.new_user_ref(domain_id=domain['id']) + new_user = self.identity_api.create_user(new_user) + self.identity_api.add_user_to_group(new_user['id'], + new_group['id']) + user_refs = self.identity_api.list_users_in_group(new_group['id']) + found = False + for x in user_refs: + if (x['id'] == new_user['id']): + found = True + self.assertNotIn('password', x) + self.assertTrue(found) + + def test_list_users_in_group_returns_not_found(self): + self.assertRaises(exception.GroupNotFound, + self.identity_api.list_users_in_group, + uuid.uuid4().hex) + + def test_list_groups_for_user(self): + domain = self._get_domain_fixture() + test_groups = [] + test_users = [] + GROUP_COUNT = 3 + USER_COUNT = 2 + + for x in range(0, USER_COUNT): + new_user = unit.new_user_ref(domain_id=domain['id']) + new_user = self.identity_api.create_user(new_user) + test_users.append(new_user) + positive_user = test_users[0] + negative_user = test_users[1] + + for x in range(0, USER_COUNT): + group_refs = self.identity_api.list_groups_for_user( + test_users[x]['id']) + self.assertEqual(0, len(group_refs)) + + for x in range(0, GROUP_COUNT): + before_count = x + after_count = x + 1 + new_group = unit.new_group_ref(domain_id=domain['id']) + new_group = self.identity_api.create_group(new_group) + test_groups.append(new_group) + + # add the user to the group and ensure that the + # group count increases by one for each + group_refs = self.identity_api.list_groups_for_user( + positive_user['id']) + self.assertEqual(before_count, len(group_refs)) + self.identity_api.add_user_to_group( + positive_user['id'], + new_group['id']) + group_refs = self.identity_api.list_groups_for_user( + positive_user['id']) + self.assertEqual(after_count, len(group_refs)) + + # Make sure the group count for the unrelated user did not change + group_refs = self.identity_api.list_groups_for_user( + negative_user['id']) + self.assertEqual(0, len(group_refs)) + + # remove the user from each group and ensure that + # the group count reduces by one for each + for x in range(0, 3): + before_count = GROUP_COUNT - x + after_count = GROUP_COUNT - x - 1 + group_refs = self.identity_api.list_groups_for_user( + positive_user['id']) + self.assertEqual(before_count, len(group_refs)) + self.identity_api.remove_user_from_group( + positive_user['id'], + test_groups[x]['id']) + group_refs = self.identity_api.list_groups_for_user( + positive_user['id']) + self.assertEqual(after_count, len(group_refs)) + # Make sure the group count for the unrelated user + # did not change + group_refs = self.identity_api.list_groups_for_user( + negative_user['id']) + self.assertEqual(0, len(group_refs)) + + def test_remove_user_from_group(self): + domain = self._get_domain_fixture() + new_group = unit.new_group_ref(domain_id=domain['id']) + new_group = self.identity_api.create_group(new_group) + new_user = unit.new_user_ref(domain_id=domain['id']) + new_user = self.identity_api.create_user(new_user) + self.identity_api.add_user_to_group(new_user['id'], + new_group['id']) + groups = self.identity_api.list_groups_for_user(new_user['id']) + self.assertIn(new_group['id'], [x['id'] for x in groups]) + self.identity_api.remove_user_from_group(new_user['id'], + new_group['id']) + groups = self.identity_api.list_groups_for_user(new_user['id']) + self.assertNotIn(new_group['id'], [x['id'] for x in groups]) + + def test_remove_user_from_group_returns_not_found(self): + domain = self._get_domain_fixture() + new_user = unit.new_user_ref(domain_id=domain['id']) + new_user = self.identity_api.create_user(new_user) + new_group = unit.new_group_ref(domain_id=domain['id']) + new_group = self.identity_api.create_group(new_group) + self.assertRaises(exception.GroupNotFound, + self.identity_api.remove_user_from_group, + new_user['id'], + uuid.uuid4().hex) + + self.assertRaises(exception.UserNotFound, + self.identity_api.remove_user_from_group, + uuid.uuid4().hex, + new_group['id']) + + self.assertRaises(exception.NotFound, + self.identity_api.remove_user_from_group, + uuid.uuid4().hex, + uuid.uuid4().hex) + + def test_group_crud(self): + domain = unit.new_domain_ref() + self.resource_api.create_domain(domain['id'], domain) + group = unit.new_group_ref(domain_id=domain['id']) + group = self.identity_api.create_group(group) + group_ref = self.identity_api.get_group(group['id']) + self.assertDictContainsSubset(group, group_ref) + + group['name'] = uuid.uuid4().hex + self.identity_api.update_group(group['id'], group) + group_ref = self.identity_api.get_group(group['id']) + self.assertDictContainsSubset(group, group_ref) + + self.identity_api.delete_group(group['id']) + self.assertRaises(exception.GroupNotFound, + self.identity_api.get_group, + group['id']) + + def test_get_group_by_name(self): + group = unit.new_group_ref(domain_id=CONF.identity.default_domain_id) + group_name = group['name'] + group = self.identity_api.create_group(group) + spoiler = unit.new_group_ref(domain_id=CONF.identity.default_domain_id) + self.identity_api.create_group(spoiler) + + group_ref = self.identity_api.get_group_by_name( + group_name, CONF.identity.default_domain_id) + self.assertDictEqual(group, group_ref) + + def test_get_group_by_name_returns_not_found(self): + self.assertRaises(exception.GroupNotFound, + self.identity_api.get_group_by_name, + uuid.uuid4().hex, + CONF.identity.default_domain_id) + + @unit.skip_if_cache_disabled('identity') + def test_cache_layer_group_crud(self): + group = unit.new_group_ref(domain_id=CONF.identity.default_domain_id) + group = self.identity_api.create_group(group) + # cache the result + group_ref = self.identity_api.get_group(group['id']) + # delete the group bypassing identity api. + domain_id, driver, entity_id = ( + self.identity_api._get_domain_driver_and_entity_id(group['id'])) + driver.delete_group(entity_id) + + self.assertEqual(group_ref, self.identity_api.get_group(group['id'])) + self.identity_api.get_group.invalidate(self.identity_api, group['id']) + self.assertRaises(exception.GroupNotFound, + self.identity_api.get_group, group['id']) + + group = unit.new_group_ref(domain_id=CONF.identity.default_domain_id) + group = self.identity_api.create_group(group) + # cache the result + self.identity_api.get_group(group['id']) + group['name'] = uuid.uuid4().hex + group_ref = self.identity_api.update_group(group['id'], group) + # after updating through identity api, get updated group + self.assertDictContainsSubset(self.identity_api.get_group(group['id']), + group_ref) + + def test_create_duplicate_group_name_fails(self): + group1 = unit.new_group_ref(domain_id=CONF.identity.default_domain_id) + group2 = unit.new_group_ref(domain_id=CONF.identity.default_domain_id, + name=group1['name']) + group1 = self.identity_api.create_group(group1) + self.assertRaises(exception.Conflict, + self.identity_api.create_group, + group2) + + def test_create_duplicate_group_name_in_different_domains(self): + new_domain = unit.new_domain_ref() + self.resource_api.create_domain(new_domain['id'], new_domain) + group1 = unit.new_group_ref(domain_id=CONF.identity.default_domain_id) + group2 = unit.new_group_ref(domain_id=new_domain['id'], + name=group1['name']) + group1 = self.identity_api.create_group(group1) + group2 = self.identity_api.create_group(group2) + + def test_move_group_between_domains(self): + domain1 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = unit.new_domain_ref() + self.resource_api.create_domain(domain2['id'], domain2) + group = unit.new_group_ref(domain_id=domain1['id']) + group = self.identity_api.create_group(group) + group['domain_id'] = domain2['id'] + # Update the group asserting that a deprecation warning is emitted + with mock.patch( + 'oslo_log.versionutils.report_deprecated_feature') as mock_dep: + self.identity_api.update_group(group['id'], group) + self.assertTrue(mock_dep.called) + + updated_group_ref = self.identity_api.get_group(group['id']) + self.assertEqual(domain2['id'], updated_group_ref['domain_id']) + + def test_move_group_between_domains_with_clashing_names_fails(self): + domain1 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = unit.new_domain_ref() + self.resource_api.create_domain(domain2['id'], domain2) + # First, create a group in domain1 + group1 = unit.new_group_ref(domain_id=domain1['id']) + group1 = self.identity_api.create_group(group1) + # Now create a group in domain2 with a potentially clashing + # name - which should work since we have domain separation + group2 = unit.new_group_ref(name=group1['name'], + domain_id=domain2['id']) + group2 = self.identity_api.create_group(group2) + # Now try and move group1 into the 2nd domain - which should + # fail since the names clash + group1['domain_id'] = domain2['id'] + self.assertRaises(exception.Conflict, + self.identity_api.update_group, + group1['id'], + group1) + + def test_user_crud(self): + user_dict = unit.new_user_ref( + domain_id=CONF.identity.default_domain_id) + del user_dict['id'] + user = self.identity_api.create_user(user_dict) + user_ref = self.identity_api.get_user(user['id']) + del user_dict['password'] + user_ref_dict = {x: user_ref[x] for x in user_ref} + self.assertDictContainsSubset(user_dict, user_ref_dict) + + user_dict['password'] = uuid.uuid4().hex + self.identity_api.update_user(user['id'], user_dict) + user_ref = self.identity_api.get_user(user['id']) + del user_dict['password'] + user_ref_dict = {x: user_ref[x] for x in user_ref} + self.assertDictContainsSubset(user_dict, user_ref_dict) + + self.identity_api.delete_user(user['id']) + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user, + user['id']) + + def test_arbitrary_attributes_are_returned_from_create_user(self): + attr_value = uuid.uuid4().hex + user_data = unit.new_user_ref( + domain_id=CONF.identity.default_domain_id, + arbitrary_attr=attr_value) + + user = self.identity_api.create_user(user_data) + + self.assertEqual(attr_value, user['arbitrary_attr']) + + def test_arbitrary_attributes_are_returned_from_get_user(self): + attr_value = uuid.uuid4().hex + user_data = unit.new_user_ref( + domain_id=CONF.identity.default_domain_id, + arbitrary_attr=attr_value) + + user_data = self.identity_api.create_user(user_data) + + user = self.identity_api.get_user(user_data['id']) + self.assertEqual(attr_value, user['arbitrary_attr']) + + def test_new_arbitrary_attributes_are_returned_from_update_user(self): + user_data = unit.new_user_ref( + domain_id=CONF.identity.default_domain_id) + + user = self.identity_api.create_user(user_data) + attr_value = uuid.uuid4().hex + user['arbitrary_attr'] = attr_value + updated_user = self.identity_api.update_user(user['id'], user) + + self.assertEqual(attr_value, updated_user['arbitrary_attr']) + + def test_updated_arbitrary_attributes_are_returned_from_update_user(self): + attr_value = uuid.uuid4().hex + user_data = unit.new_user_ref( + domain_id=CONF.identity.default_domain_id, + arbitrary_attr=attr_value) + + new_attr_value = uuid.uuid4().hex + user = self.identity_api.create_user(user_data) + user['arbitrary_attr'] = new_attr_value + updated_user = self.identity_api.update_user(user['id'], user) + + self.assertEqual(new_attr_value, updated_user['arbitrary_attr']) + + def test_user_update_and_user_get_return_same_response(self): + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + + user = self.identity_api.create_user(user) + + updated_user = {'enabled': False} + updated_user_ref = self.identity_api.update_user( + user['id'], updated_user) + + # SQL backend adds 'extra' field + updated_user_ref.pop('extra', None) + + self.assertIs(False, updated_user_ref['enabled']) + + user_ref = self.identity_api.get_user(user['id']) + self.assertDictEqual(updated_user_ref, user_ref) + + +class FilterTests(filtering.FilterTests): + def test_list_entities_filtered(self): + for entity in ['user', 'group', 'project']: + # Create 20 entities + entity_list = self._create_test_data(entity, 20) + + # Try filtering to get one an exact item out of the list + hints = driver_hints.Hints() + hints.add_filter('name', entity_list[10]['name']) + entities = self._list_entities(entity)(hints=hints) + self.assertEqual(1, len(entities)) + self.assertEqual(entity_list[10]['id'], entities[0]['id']) + # Check the driver has removed the filter from the list hints + self.assertFalse(hints.get_exact_filter_by_name('name')) + self._delete_test_data(entity, entity_list) + + def test_list_users_inexact_filtered(self): + # Create 20 users, some with specific names. We set the names at create + # time (rather than updating them), since the LDAP driver does not + # support name updates. + user_name_data = { + # user index: name for user + 5: 'The', + 6: 'The Ministry', + 7: 'The Ministry of', + 8: 'The Ministry of Silly', + 9: 'The Ministry of Silly Walks', + # ...and one for useful case insensitivity testing + 10: 'The ministry of silly walks OF' + } + user_list = self._create_test_data( + 'user', 20, domain_id=CONF.identity.default_domain_id, + name_dict=user_name_data) + + hints = driver_hints.Hints() + hints.add_filter('name', 'ministry', comparator='contains') + users = self.identity_api.list_users(hints=hints) + self.assertEqual(5, len(users)) + self._match_with_list(users, user_list, + list_start=6, list_end=11) + # TODO(henry-nash) Check inexact filter has been removed. + + hints = driver_hints.Hints() + hints.add_filter('name', 'The', comparator='startswith') + users = self.identity_api.list_users(hints=hints) + self.assertEqual(6, len(users)) + self._match_with_list(users, user_list, + list_start=5, list_end=11) + # TODO(henry-nash) Check inexact filter has been removed. + + hints = driver_hints.Hints() + hints.add_filter('name', 'of', comparator='endswith') + users = self.identity_api.list_users(hints=hints) + self.assertEqual(2, len(users)) + # We can't assume we will get back the users in any particular order + self.assertIn(user_list[7]['id'], [users[0]['id'], users[1]['id']]) + self.assertIn(user_list[10]['id'], [users[0]['id'], users[1]['id']]) + # TODO(henry-nash) Check inexact filter has been removed. + + # TODO(henry-nash): Add some case sensitive tests. However, + # these would be hard to validate currently, since: + # + # For SQL, the issue is that MySQL 0.7, by default, is installed in + # case insensitive mode (which is what is run by default for our + # SQL backend tests). For production deployments. OpenStack + # assumes a case sensitive database. For these tests, therefore, we + # need to be able to check the sensitivity of the database so as to + # know whether to run case sensitive tests here. + # + # For LDAP/AD, although dependent on the schema being used, attributes + # are typically configured to be case aware, but not case sensitive. + + self._delete_test_data('user', user_list) + + def _groups_for_user_data(self): + number_of_groups = 10 + group_name_data = { + # entity index: name for entity + 5: 'The', + 6: 'The Ministry', + 9: 'The Ministry of Silly Walks', + } + group_list = self._create_test_data( + 'group', number_of_groups, + domain_id=CONF.identity.default_domain_id, + name_dict=group_name_data) + user_list = self._create_test_data('user', 2) + + for group in range(7): + # Create membership, including with two out of the three groups + # with well know names + self.identity_api.add_user_to_group(user_list[0]['id'], + group_list[group]['id']) + # ...and some spoiler memberships + for group in range(7, number_of_groups): + self.identity_api.add_user_to_group(user_list[1]['id'], + group_list[group]['id']) + + return group_list, user_list + + def test_groups_for_user_inexact_filtered(self): + """Test use of filtering doesn't break groups_for_user listing. + + Some backends may use filtering to achieve the list of groups for a + user, so test that it can combine a second filter. + + Test Plan: + + - Create 10 groups, some with names we can filter on + - Create 2 users + - Assign 1 of those users to most of the groups, including some of the + well known named ones + - Assign the other user to other groups as spoilers + - Ensure that when we list groups for users with a filter on the group + name, both restrictions have been enforced on what is returned. + + """ + group_list, user_list = self._groups_for_user_data() + + hints = driver_hints.Hints() + hints.add_filter('name', 'Ministry', comparator='contains') + groups = self.identity_api.list_groups_for_user( + user_list[0]['id'], hints=hints) + # We should only get back one group, since of the two that contain + # 'Ministry' the user only belongs to one. + self.assertThat(len(groups), matchers.Equals(1)) + self.assertEqual(group_list[6]['id'], groups[0]['id']) + + hints = driver_hints.Hints() + hints.add_filter('name', 'The', comparator='startswith') + groups = self.identity_api.list_groups_for_user( + user_list[0]['id'], hints=hints) + # We should only get back 2 out of the 3 groups that start with 'The' + # hence showing that both "filters" have been applied + self.assertThat(len(groups), matchers.Equals(2)) + self.assertIn(group_list[5]['id'], [groups[0]['id'], groups[1]['id']]) + self.assertIn(group_list[6]['id'], [groups[0]['id'], groups[1]['id']]) + + hints.add_filter('name', 'The', comparator='endswith') + groups = self.identity_api.list_groups_for_user( + user_list[0]['id'], hints=hints) + # We should only get back one group since it is the only one that + # ends with 'The' + self.assertThat(len(groups), matchers.Equals(1)) + self.assertEqual(group_list[5]['id'], groups[0]['id']) + + self._delete_test_data('user', user_list) + self._delete_test_data('group', group_list) + + def test_groups_for_user_exact_filtered(self): + """Test exact filters doesn't break groups_for_user listing.""" + group_list, user_list = self._groups_for_user_data() + hints = driver_hints.Hints() + hints.add_filter('name', 'The Ministry', comparator='equals') + groups = self.identity_api.list_groups_for_user( + user_list[0]['id'], hints=hints) + # We should only get back 1 out of the 3 groups with name 'The + # Ministry' hence showing that both "filters" have been applied. + self.assertEqual(1, len(groups)) + self.assertEqual(group_list[6]['id'], groups[0]['id']) + self._delete_test_data('user', user_list) + self._delete_test_data('group', group_list) + + def _get_user_name_field_size(self): + """Return the size of the user name field for the backend. + + Subclasses can override this method to indicate that the user name + field is limited in length. The user name is the field used in the test + that validates that a filter value works even if it's longer than a + field. + + If the backend doesn't limit the value length then return None. + + """ + return None + + def test_filter_value_wider_than_field(self): + # If a filter value is given that's larger than the field in the + # backend then no values are returned. + + user_name_field_size = self._get_user_name_field_size() + + if user_name_field_size is None: + # The backend doesn't limit the size of the user name, so pass this + # test. + return + + # Create some users just to make sure would return something if the + # filter was ignored. + self._create_test_data('user', 2) + + hints = driver_hints.Hints() + value = 'A' * (user_name_field_size + 1) + hints.add_filter('name', value) + users = self.identity_api.list_users(hints=hints) + self.assertEqual([], users) + + def _list_users_in_group_data(self): + number_of_users = 10 + user_name_data = { + 1: 'Arthur Conan Doyle', + 3: 'Arthur Rimbaud', + 9: 'Arthur Schopenhauer', + } + user_list = self._create_test_data( + 'user', number_of_users, + domain_id=CONF.identity.default_domain_id, + name_dict=user_name_data) + group = self._create_one_entity( + 'group', CONF.identity.default_domain_id, 'Great Writers') + for i in range(7): + self.identity_api.add_user_to_group(user_list[i]['id'], + group['id']) + + return user_list, group + + def test_list_users_in_group_inexact_filtered(self): + user_list, group = self._list_users_in_group_data() + + hints = driver_hints.Hints() + hints.add_filter('name', 'Arthur', comparator='contains') + users = self.identity_api.list_users_in_group(group['id'], hints=hints) + self.assertThat(len(users), matchers.Equals(2)) + self.assertIn(user_list[1]['id'], [users[0]['id'], users[1]['id']]) + self.assertIn(user_list[3]['id'], [users[0]['id'], users[1]['id']]) + + hints = driver_hints.Hints() + hints.add_filter('name', 'Arthur', comparator='startswith') + users = self.identity_api.list_users_in_group(group['id'], hints=hints) + self.assertThat(len(users), matchers.Equals(2)) + self.assertIn(user_list[1]['id'], [users[0]['id'], users[1]['id']]) + self.assertIn(user_list[3]['id'], [users[0]['id'], users[1]['id']]) + + hints = driver_hints.Hints() + hints.add_filter('name', 'Doyle', comparator='endswith') + users = self.identity_api.list_users_in_group(group['id'], hints=hints) + self.assertThat(len(users), matchers.Equals(1)) + self.assertEqual(user_list[1]['id'], users[0]['id']) + + self._delete_test_data('user', user_list) + self._delete_entity('group')(group['id']) + + def test_list_users_in_group_exact_filtered(self): + hints = driver_hints.Hints() + user_list, group = self._list_users_in_group_data() + hints.add_filter('name', 'Arthur Rimbaud', comparator='equals') + users = self.identity_api.list_users_in_group(group['id'], hints=hints) + self.assertEqual(1, len(users)) + self.assertEqual(user_list[3]['id'], users[0]['id']) + self._delete_test_data('user', user_list) + self._delete_entity('group')(group['id']) + + +class LimitTests(filtering.FilterTests): + ENTITIES = ['user', 'group', 'project'] + + def setUp(self): + """Setup for Limit Test Cases.""" + self.entity_lists = {} + + for entity in self.ENTITIES: + # Create 20 entities + self.entity_lists[entity] = self._create_test_data(entity, 20) + self.addCleanup(self.clean_up_entities) + + def clean_up_entities(self): + """Clean up entity test data from Limit Test Cases.""" + for entity in self.ENTITIES: + self._delete_test_data(entity, self.entity_lists[entity]) + del self.entity_lists + + def _test_list_entity_filtered_and_limited(self, entity): + self.config_fixture.config(list_limit=10) + # Should get back just 10 entities + hints = driver_hints.Hints() + entities = self._list_entities(entity)(hints=hints) + self.assertEqual(hints.limit['limit'], len(entities)) + self.assertTrue(hints.limit['truncated']) + + # Override with driver specific limit + if entity == 'project': + self.config_fixture.config(group='resource', list_limit=5) + else: + self.config_fixture.config(group='identity', list_limit=5) + + # Should get back just 5 users + hints = driver_hints.Hints() + entities = self._list_entities(entity)(hints=hints) + self.assertEqual(hints.limit['limit'], len(entities)) + + # Finally, let's pretend we want to get the full list of entities, + # even with the limits set, as part of some internal calculation. + # Calling the API without a hints list should achieve this, and + # return at least the 20 entries we created (there may be other + # entities lying around created by other tests/setup). + entities = self._list_entities(entity)() + self.assertTrue(len(entities) >= 20) + self._match_with_list(self.entity_lists[entity], entities) + + def test_list_users_filtered_and_limited(self): + self._test_list_entity_filtered_and_limited('user') + + def test_list_groups_filtered_and_limited(self): + self._test_list_entity_filtered_and_limited('group') + + def test_list_projects_filtered_and_limited(self): + self._test_list_entity_filtered_and_limited('project') diff --git a/keystone-moon/keystone/tests/unit/identity/test_controllers.py b/keystone-moon/keystone/tests/unit/identity/test_controllers.py new file mode 100644 index 00000000..ed2fe3ff --- /dev/null +++ b/keystone-moon/keystone/tests/unit/identity/test_controllers.py @@ -0,0 +1,65 @@ +# Copyright 2016 IBM Corp. +# +# 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 oslo_config import cfg + +from keystone import exception +from keystone.identity import controllers +from keystone.tests import unit +from keystone.tests.unit.ksfixtures import database + + +CONF = cfg.CONF + +_ADMIN_CONTEXT = {'is_admin': True, 'query_string': {}} + + +class UserTestCaseNoDefaultDomain(unit.TestCase): + + def setUp(self): + super(UserTestCaseNoDefaultDomain, self).setUp() + self.useFixture(database.Database()) + self.load_backends() + self.user_controller = controllers.User() + + def test_setup(self): + # Other tests in this class assume there's no default domain, so make + # sure the setUp worked as expected. + self.assertRaises( + exception.DomainNotFound, + self.resource_api.get_domain, CONF.identity.default_domain_id) + + def test_get_users(self): + # When list_users is done and there's no default domain, the result is + # an empty list. + res = self.user_controller.get_users(_ADMIN_CONTEXT) + self.assertEqual([], res['users']) + + def test_get_user_by_name(self): + # When get_user_by_name is done and there's no default domain, the + # result is 404 Not Found + user_name = uuid.uuid4().hex + self.assertRaises( + exception.UserNotFound, + self.user_controller.get_user_by_name, _ADMIN_CONTEXT, user_name) + + def test_create_user(self): + # When a user is created using the v2 controller and there's no default + # domain, it doesn't fail with can't find domain (a default domain is + # created) + user = {'name': uuid.uuid4().hex} + self.user_controller.create_user(_ADMIN_CONTEXT, user) + # If the above doesn't fail then this is successful. diff --git a/keystone-moon/keystone/tests/unit/identity/test_core.py b/keystone-moon/keystone/tests/unit/identity/test_core.py index e9845401..39f3c701 100644 --- a/keystone-moon/keystone/tests/unit/identity/test_core.py +++ b/keystone-moon/keystone/tests/unit/identity/test_core.py @@ -138,7 +138,7 @@ class TestDatabaseDomainConfigs(unit.TestCase): def test_loading_config_from_database(self): self.config_fixture.config(domain_configurations_from_database=True, group='identity') - domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + domain = unit.new_domain_ref() self.resource_api.create_domain(domain['id'], domain) # Override two config options for our domain conf = {'ldap': {'url': uuid.uuid4().hex, @@ -165,7 +165,7 @@ class TestDatabaseDomainConfigs(unit.TestCase): # Now turn off using database domain configuration and check that the # default config file values are now seen instead of the overrides. CONF.set_override('domain_configurations_from_database', False, - 'identity') + 'identity', enforce_type=True) domain_config = identity.DomainConfigs() domain_config.setup_domain_drivers(fake_standard_driver, self.resource_api) diff --git a/keystone-moon/keystone/tests/unit/identity_mapping.py b/keystone-moon/keystone/tests/unit/identity_mapping.py index 7fb8063f..4ba4f0c2 100644 --- a/keystone-moon/keystone/tests/unit/identity_mapping.py +++ b/keystone-moon/keystone/tests/unit/identity_mapping.py @@ -17,7 +17,6 @@ from keystone.identity.mapping_backends import sql as mapping_sql def list_id_mappings(): """List all id_mappings for testing purposes.""" - - a_session = sql.get_session() - refs = a_session.query(mapping_sql.IDMapping).all() - return [x.to_dict() for x in refs] + with sql.session_for_read() as session: + refs = session.query(mapping_sql.IDMapping).all() + return [x.to_dict() for x in refs] diff --git a/keystone-moon/keystone/tests/unit/ksfixtures/__init__.py b/keystone-moon/keystone/tests/unit/ksfixtures/__init__.py index 81b80298..4b914752 100644 --- a/keystone-moon/keystone/tests/unit/ksfixtures/__init__.py +++ b/keystone-moon/keystone/tests/unit/ksfixtures/__init__.py @@ -11,5 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from keystone.tests.unit.ksfixtures.auth_plugins import ConfigAuthPlugins # noqa from keystone.tests.unit.ksfixtures.cache import Cache # noqa from keystone.tests.unit.ksfixtures.key_repository import KeyRepository # noqa +from keystone.tests.unit.ksfixtures.policy import Policy # noqa diff --git a/keystone-moon/keystone/tests/unit/ksfixtures/appserver.py b/keystone-moon/keystone/tests/unit/ksfixtures/appserver.py index ea1e6255..a23b804f 100644 --- a/keystone-moon/keystone/tests/unit/ksfixtures/appserver.py +++ b/keystone-moon/keystone/tests/unit/ksfixtures/appserver.py @@ -29,8 +29,7 @@ ADMIN = 'admin' class AppServer(fixtures.Fixture): - """A fixture for managing an application server instance. - """ + """A fixture for managing an application server instance.""" def __init__(self, config, name, cert=None, key=None, ca=None, cert_required=False, host='127.0.0.1', port=0): @@ -72,7 +71,8 @@ class AppServer(fixtures.Fixture): def _update_config_opt(self): """Updates the config with the actual port used.""" opt_name = self._get_config_option_for_section_name() - CONF.set_override(opt_name, self.port, group='eventlet_server') + CONF.set_override(opt_name, self.port, group='eventlet_server', + enforce_type=True) def _get_config_option_for_section_name(self): """Maps Paster config section names to port option names.""" diff --git a/keystone-moon/keystone/tests/unit/ksfixtures/auth_plugins.py b/keystone-moon/keystone/tests/unit/ksfixtures/auth_plugins.py new file mode 100644 index 00000000..68ba6f3a --- /dev/null +++ b/keystone-moon/keystone/tests/unit/ksfixtures/auth_plugins.py @@ -0,0 +1,34 @@ +# 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 fixtures + +from keystone.common import config as common_cfg + + +class ConfigAuthPlugins(fixtures.Fixture): + """A fixture for setting up and tearing down a auth plugins.""" + + def __init__(self, config_fixture, methods, **method_classes): + super(ConfigAuthPlugins, self).__init__() + self.methods = methods + self.config_fixture = config_fixture + self.method_classes = method_classes + + def setUp(self): + super(ConfigAuthPlugins, self).setUp() + if self.methods: + self.config_fixture.config(group='auth', methods=self.methods) + common_cfg.setup_authentication() + if self.method_classes: + self.config_fixture.config(group='auth', **self.method_classes) diff --git a/keystone-moon/keystone/tests/unit/ksfixtures/cache.py b/keystone-moon/keystone/tests/unit/ksfixtures/cache.py index 74566f1e..e0833ae2 100644 --- a/keystone-moon/keystone/tests/unit/ksfixtures/cache.py +++ b/keystone-moon/keystone/tests/unit/ksfixtures/cache.py @@ -13,11 +13,17 @@ import fixtures +from keystone import catalog from keystone.common import cache +CACHE_REGIONS = (cache.CACHE_REGION, catalog.COMPUTED_CATALOG_REGION) + + class Cache(fixtures.Fixture): - """A fixture for setting up and tearing down the cache between test cases. + """A fixture for setting up the cache between test cases. + + This will also tear down an existing cache if one is already configured. """ def setUp(self): @@ -29,8 +35,9 @@ class Cache(fixtures.Fixture): # NOTE(morganfainberg): The only way to reconfigure the CacheRegion # object on each setUp() call is to remove the .backend property. - if cache.REGION.is_configured: - del cache.REGION.backend + for region in CACHE_REGIONS: + if region.is_configured: + del region.backend - # ensure the cache region instance is setup - cache.configure_cache_region(cache.REGION) + # ensure the cache region instance is setup + cache.configure_cache(region=region) diff --git a/keystone-moon/keystone/tests/unit/ksfixtures/database.py b/keystone-moon/keystone/tests/unit/ksfixtures/database.py index 6f23a99d..52c35cee 100644 --- a/keystone-moon/keystone/tests/unit/ksfixtures/database.py +++ b/keystone-moon/keystone/tests/unit/ksfixtures/database.py @@ -28,12 +28,13 @@ CONF = cfg.CONF def run_once(f): """A decorator to ensure the decorated function is only executed once. - The decorated function cannot expect any arguments. + The decorated function is assumed to have a one parameter. + """ @functools.wraps(f) - def wrapper(): + def wrapper(one): if not wrapper.already_ran: - f() + f(one) wrapper.already_ran = True wrapper.already_ran = False return wrapper @@ -51,7 +52,7 @@ def initialize_sql_session(): @run_once -def _load_sqlalchemy_models(): +def _load_sqlalchemy_models(version_specifiers): """Find all modules containing SQLAlchemy models and import them. This creates more consistent, deterministic test runs because tables @@ -66,6 +67,24 @@ def _load_sqlalchemy_models(): as more models are imported. Importing all models at the start of the test run avoids this problem. + version_specifiers is a dict that contains any specific driver versions + that have been requested. The dict is of the form: + + { : {'versioned_backend' : , + 'versionless_backend' : } + } + + For example: + + {'keystone.assignment': {'versioned_backend' : 'V8_backends', + 'versionless_backend' : 'backends'}, + 'keystone.identity': {'versioned_backend' : 'V9_backends', + 'versionless_backend' : 'backends'} + } + + The version_specifiers will be used to load the correct driver. The + algorithm for this assumes that versioned drivers begin in 'V'. + """ keystone_root = os.path.normpath(os.path.join( os.path.dirname(__file__), '..', '..', '..')) @@ -78,25 +97,59 @@ def _load_sqlalchemy_models(): # The root will be prefixed with an instance of os.sep, which will # make the root after replacement '.', the 'keystone' part # of the module path is always added to the front - module_name = ('keystone.%s.sql' % + module_root = ('keystone.%s' % root.replace(os.sep, '.').lstrip('.')) + module_components = module_root.split('.') + module_without_backends = '' + for x in range(0, len(module_components) - 1): + module_without_backends += module_components[x] + '.' + module_without_backends = module_without_backends.rstrip('.') + this_backend = module_components[len(module_components) - 1] + + # At this point module_without_backends might be something like + # 'keystone.assignment', while this_backend might be something + # 'V8_backends'. + + if module_without_backends.startswith('keystone.contrib'): + # All the sql modules have now been moved into the core tree + # so no point in loading these again here (and, in fact, doing + # so might break trying to load a versioned driver. + continue + + if module_without_backends in version_specifiers: + # OK, so there is a request for a specific version of this one. + # We therefore should skip any other versioned backend as well + # as the non-versioned one. + version = version_specifiers[module_without_backends] + if ((this_backend != version['versioned_backend'] and + this_backend.startswith('V')) or + this_backend == version['versionless_backend']): + continue + else: + # No versioned driver requested, so ignore any that are + # versioned + if this_backend.startswith('V'): + continue + + module_name = module_root + '.sql' __import__(module_name) class Database(fixtures.Fixture): - """A fixture for setting up and tearing down a database. - - """ + """A fixture for setting up and tearing down a database.""" - def __init__(self): + def __init__(self, version_specifiers=None): super(Database, self).__init__() initialize_sql_session() - _load_sqlalchemy_models() + if version_specifiers is None: + version_specifiers = {} + _load_sqlalchemy_models(version_specifiers) def setUp(self): super(Database, self).setUp() - self.engine = sql.get_engine() + with sql.session_for_write() as session: + self.engine = session.get_bind() self.addCleanup(sql.cleanup) sql.ModelBase.metadata.create_all(bind=self.engine) self.addCleanup(sql.ModelBase.metadata.drop_all, bind=self.engine) diff --git a/keystone-moon/keystone/tests/unit/ksfixtures/hacking.py b/keystone-moon/keystone/tests/unit/ksfixtures/hacking.py index 918087ad..9977b206 100644 --- a/keystone-moon/keystone/tests/unit/ksfixtures/hacking.py +++ b/keystone-moon/keystone/tests/unit/ksfixtures/hacking.py @@ -112,73 +112,6 @@ class HackingCode(fixtures.Fixture): (8, 8, 'K004'), ]} - assert_no_translations_for_debug_logging = { - 'code': """ - import logging - import logging as stlib_logging - from keystone.i18n import _ - from keystone.i18n import _ as oslo_i18n - from oslo_log import log - from oslo_log import log as oslo_logging - - # stdlib logging - L0 = logging.getLogger() - L0.debug(_('text')) - class C: - def __init__(self): - L0.debug(oslo_i18n('text', {})) - - # stdlib logging w/ alias and specifying a logger - class C: - def __init__(self): - self.L1 = logging.getLogger(__name__) - def m(self): - self.L1.debug( - _('text'), {} - ) - - # oslo logging and specifying a logger - L2 = logging.getLogger(__name__) - L2.debug(oslo_i18n('text')) - - # oslo logging w/ alias - class C: - def __init__(self): - self.L3 = oslo_logging.getLogger() - self.L3.debug(_('text')) - - # translation on a separate line - msg = _('text') - L2.debug(msg) - - # this should not fail - if True: - msg = _('message %s') % X - L2.error(msg) - raise TypeError(msg) - if True: - msg = 'message' - L2.debug(msg) - - # this should not fail - if True: - if True: - msg = _('message') - else: - msg = _('message') - L2.debug(msg) - raise Exception(msg) - """, - 'expected_errors': [ - (10, 9, 'K005'), - (13, 17, 'K005'), - (21, 12, 'K005'), - (26, 9, 'K005'), - (32, 22, 'K005'), - (36, 9, 'K005'), - ] - } - dict_constructor = { 'code': """ lower_res = {k.lower(): v for k, v in six.iteritems(res[1])} @@ -219,12 +152,12 @@ class HackingLogging(fixtures.Fixture): LOG.info(_('text')) class C: def __init__(self): - LOG.warn(oslo_i18n('text', {})) - LOG.warn(_LW('text', {})) + LOG.warning(oslo_i18n('text', {})) + LOG.warning(_LW('text', {})) """, 'expected_errors': [ (3, 9, 'K006'), - (6, 17, 'K006'), + (6, 20, 'K006'), ], }, { @@ -287,13 +220,13 @@ class HackingLogging(fixtures.Fixture): LOG = logging.getLogger() # ensure the correct helper is being used - LOG.warn(_LI('this should cause an error')) + LOG.warning(_LI('this should cause an error')) # debug should not allow any helpers either LOG.debug(_LI('this should cause an error')) """, 'expected_errors': [ - (4, 9, 'K006'), + (4, 12, 'K006'), (7, 10, 'K005'), ], }, @@ -302,7 +235,7 @@ class HackingLogging(fixtures.Fixture): # this should not be an error L = log.getLogger(__name__) msg = _('text') - L.warn(msg) + L.warning(msg) raise Exception(msg) """, 'expected_errors': [], @@ -312,7 +245,7 @@ class HackingLogging(fixtures.Fixture): L = log.getLogger(__name__) def f(): msg = _('text') - L2.warn(msg) + L2.warning(msg) something = True # add an extra statement here raise Exception(msg) """, @@ -323,11 +256,11 @@ class HackingLogging(fixtures.Fixture): LOG = log.getLogger(__name__) def func(): msg = _('text') - LOG.warn(msg) + LOG.warning(msg) raise Exception('some other message') """, 'expected_errors': [ - (4, 13, 'K006'), + (4, 16, 'K006'), ], }, { @@ -337,7 +270,7 @@ class HackingLogging(fixtures.Fixture): msg = _('text') else: msg = _('text') - LOG.warn(msg) + LOG.warning(msg) raise Exception(msg) """, 'expected_errors': [ @@ -350,28 +283,28 @@ class HackingLogging(fixtures.Fixture): msg = _('text') else: msg = _('text') - LOG.warn(msg) + LOG.warning(msg) """, 'expected_errors': [ - (6, 9, 'K006'), + (6, 12, 'K006'), ], }, { 'code': """ LOG = log.getLogger(__name__) msg = _LW('text') - LOG.warn(msg) + LOG.warning(msg) raise Exception(msg) """, 'expected_errors': [ - (3, 9, 'K007'), + (3, 12, 'K007'), ], }, { 'code': """ LOG = log.getLogger(__name__) msg = _LW('text') - LOG.warn(msg) + LOG.warning(msg) msg = _('something else') raise Exception(msg) """, @@ -381,18 +314,18 @@ class HackingLogging(fixtures.Fixture): 'code': """ LOG = log.getLogger(__name__) msg = _LW('hello %s') % 'world' - LOG.warn(msg) + LOG.warning(msg) raise Exception(msg) """, 'expected_errors': [ - (3, 9, 'K007'), + (3, 12, 'K007'), ], }, { 'code': """ LOG = log.getLogger(__name__) msg = _LW('hello %s') % 'world' - LOG.warn(msg) + LOG.warning(msg) """, 'expected_errors': [], }, @@ -409,3 +342,76 @@ class HackingLogging(fixtures.Fixture): 'expected_errors': [], }, ] + + assert_not_using_deprecated_warn = { + 'code': """ + # Logger.warn has been deprecated in Python3 in favor of + # Logger.warning + LOG = log.getLogger(__name__) + LOG.warn(_LW('text')) + """, + 'expected_errors': [ + (4, 9, 'K009'), + ], + } + + assert_no_translations_for_debug_logging = { + 'code': """ + # stdlib logging + L0 = logging.getLogger() + L0.debug(_('text')) + class C: + def __init__(self): + L0.debug(oslo_i18n('text', {})) + + # stdlib logging w/ alias and specifying a logger + class C: + def __init__(self): + self.L1 = logging.getLogger(__name__) + def m(self): + self.L1.debug( + _('text'), {} + ) + + # oslo logging and specifying a logger + L2 = logging.getLogger(__name__) + L2.debug(oslo_i18n('text')) + + # oslo logging w/ alias + class C: + def __init__(self): + self.L3 = oslo_logging.getLogger() + self.L3.debug(_('text')) + + # translation on a separate line + msg = _('text') + L2.debug(msg) + + # this should not fail + if True: + msg = _('message %s') % X + L2.error(msg) + raise TypeError(msg) + if True: + msg = 'message' + L2.debug(msg) + + # this should not fail + if True: + if True: + msg = _('message') + else: + msg = _('message') + L2.debug(msg) + raise Exception(msg) + """, + 'expected_errors': [ + (3, 9, 'K005'), + (6, 17, 'K005'), + (14, 12, 'K005'), + (19, 9, 'K005'), + (25, 22, 'K005'), + (29, 9, 'K005'), + ] + } + diff --git a/keystone-moon/keystone/tests/unit/ksfixtures/ldapdb.py b/keystone-moon/keystone/tests/unit/ksfixtures/ldapdb.py index b2cbe067..6cd8cc0b 100644 --- a/keystone-moon/keystone/tests/unit/ksfixtures/ldapdb.py +++ b/keystone-moon/keystone/tests/unit/ksfixtures/ldapdb.py @@ -19,8 +19,7 @@ from keystone.tests.unit import fakeldap class LDAPDatabase(fixtures.Fixture): - """A fixture for setting up and tearing down an LDAP database. - """ + """A fixture for setting up and tearing down an LDAP database.""" def setUp(self): super(LDAPDatabase, self).setUp() diff --git a/keystone-moon/keystone/tests/unit/ksfixtures/policy.py b/keystone-moon/keystone/tests/unit/ksfixtures/policy.py new file mode 100644 index 00000000..b883f980 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/ksfixtures/policy.py @@ -0,0 +1,33 @@ +# 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 fixtures +from oslo_policy import opts + +from keystone.policy.backends import rules + + +class Policy(fixtures.Fixture): + """A fixture for working with policy configuration.""" + + def __init__(self, policy_file, config_fixture): + self._policy_file = policy_file + self._config_fixture = config_fixture + + def setUp(self): + super(Policy, self).setUp() + opts.set_defaults(self._config_fixture.conf) + self._config_fixture.config(group='oslo_policy', + policy_file=self._policy_file) + rules.init() + self.addCleanup(rules.reset) diff --git a/keystone-moon/keystone/tests/unit/mapping_fixtures.py b/keystone-moon/keystone/tests/unit/mapping_fixtures.py index 94b07133..9dc980aa 100644 --- a/keystone-moon/keystone/tests/unit/mapping_fixtures.py +++ b/keystone-moon/keystone/tests/unit/mapping_fixtures.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + # 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 @@ -463,6 +465,30 @@ MAPPING_TESTER_REGEX = { ] } + +MAPPING_DIRECT_MAPPING_THROUGH_KEYWORD = { + "rules": [ + { + "local": [ + { + "user": "{0}" + }, + { + "group": TESTER_GROUP_ID + } + ], + "remote": [ + { + "type": "UserName", + "any_one_of": [ + "bwilliams" + ] + } + ] + } + ] +} + MAPPING_DEVELOPER_REGEX = { "rules": [ { @@ -760,7 +786,7 @@ MAPPING_GROUPS_BLACKLIST = { ] } -# Excercise all possibilities of user identitfication. Values are hardcoded on +# Exercise all possibilities of user identification. Values are hardcoded on # purpose. MAPPING_USER_IDS = { "rules": [ @@ -1036,6 +1062,78 @@ MAPPING_WITH_DOMAINID_ONLY = { ] } +MAPPING_GROUPS_IDS_WHITELIST = { + "rules": [ + { + "local": [ + { + "user": { + "name": "{0}" + } + }, + { + "group_ids": "{1}" + }, + { + "group": { + "id": "{2}" + } + } + ], + "remote": [ + { + "type": "name" + }, + { + "type": "group_ids", + "whitelist": [ + "abc123", "ghi789", "321cba" + ] + }, + { + "type": "group" + } + ] + } + ] +} + +MAPPING_GROUPS_IDS_BLACKLIST = { + "rules": [ + { + "local": [ + { + "user": { + "name": "{0}" + } + }, + { + "group_ids": "{1}" + }, + { + "group": { + "id": "{2}" + } + } + ], + "remote": [ + { + "type": "name" + }, + { + "type": "group_ids", + "blacklist": [ + "def456" + ] + }, + { + "type": "group" + } + ] + } + ] +} + # Mapping used by tokenless test cases, it maps the domain_name only. MAPPING_WITH_DOMAINNAME_ONLY = { 'rules': [ @@ -1184,6 +1282,26 @@ MAPPING_GROUPS_WHITELIST_PASS_THROUGH = { ] } +MAPPING_BAD_LOCAL_SETUP = { + "rules": [ + { + "local": [ + { + "user": { + "name": "{0}", + "domain": {"id": "default"} + }, + "whatisthis": "local" + } + ], + "remote": [ + { + "type": "UserName" + } + ] + } + ] +} EMPLOYEE_ASSERTION = { 'Email': 'tim@example.com', @@ -1310,3 +1428,59 @@ UNMATCHED_GROUP_ASSERTION = { 'REMOTE_USER': 'Any Momoose', 'REMOTE_USER_GROUPS': 'EXISTS;NO_EXISTS' } + +GROUP_IDS_ASSERTION = { + 'name': 'opilotte', + 'group_ids': 'abc123;def456;ghi789', + 'group': 'klm012' +} + +GROUP_IDS_ASSERTION_ONLY_ONE_GROUP = { + 'name': 'opilotte', + 'group_ids': '321cba', + 'group': '210mlk' +} + +UNICODE_NAME_ASSERTION = { + 'PFX_Email': 'jon@example.com', + 'PFX_UserName': 'jonkare', + 'PFX_FirstName': 'Jon Kåre', + 'PFX_LastName': 'Hellån', + 'PFX_orgPersonType': 'Admin;Chief' +} + +MAPPING_UNICODE = { + "rules": [ + { + "local": [ + { + "user": { + "name": "{0} {1}", + "email": "{2}" + }, + "group": { + "id": EMPLOYEE_GROUP_ID + } + } + ], + "remote": [ + { + "type": "PFX_FirstName" + }, + { + "type": "PFX_LastName" + }, + { + "type": "PFX_Email" + }, + { + "type": "PFX_orgPersonType", + "any_one_of": [ + "Admin", + "Big Cheese" + ] + } + ] + }, + ], +} diff --git a/keystone-moon/keystone/tests/unit/policy/__init__.py b/keystone-moon/keystone/tests/unit/policy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/unit/policy/test_backends.py b/keystone-moon/keystone/tests/unit/policy/test_backends.py new file mode 100644 index 00000000..7b672420 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/policy/test_backends.py @@ -0,0 +1,86 @@ +# 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 +from keystone.tests import unit + + +class PolicyTests(object): + def test_create(self): + ref = unit.new_policy_ref() + res = self.policy_api.create_policy(ref['id'], ref) + self.assertDictEqual(ref, res) + + def test_get(self): + ref = unit.new_policy_ref() + res = self.policy_api.create_policy(ref['id'], ref) + + res = self.policy_api.get_policy(ref['id']) + self.assertDictEqual(ref, res) + + def test_list(self): + ref = unit.new_policy_ref() + self.policy_api.create_policy(ref['id'], ref) + + res = self.policy_api.list_policies() + res = [x for x in res if x['id'] == ref['id']][0] + self.assertDictEqual(ref, res) + + def test_update(self): + ref = unit.new_policy_ref() + self.policy_api.create_policy(ref['id'], ref) + orig = ref + + ref = unit.new_policy_ref() + + # (cannot change policy ID) + self.assertRaises(exception.ValidationError, + self.policy_api.update_policy, + orig['id'], + ref) + + ref['id'] = orig['id'] + res = self.policy_api.update_policy(orig['id'], ref) + self.assertDictEqual(ref, res) + + def test_delete(self): + ref = unit.new_policy_ref() + self.policy_api.create_policy(ref['id'], ref) + + self.policy_api.delete_policy(ref['id']) + self.assertRaises(exception.PolicyNotFound, + self.policy_api.delete_policy, + ref['id']) + self.assertRaises(exception.PolicyNotFound, + self.policy_api.get_policy, + ref['id']) + res = self.policy_api.list_policies() + self.assertFalse(len([x for x in res if x['id'] == ref['id']])) + + def test_get_policy_returns_not_found(self): + self.assertRaises(exception.PolicyNotFound, + self.policy_api.get_policy, + uuid.uuid4().hex) + + def test_update_policy_returns_not_found(self): + ref = unit.new_policy_ref() + self.assertRaises(exception.PolicyNotFound, + self.policy_api.update_policy, + ref['id'], + ref) + + def test_delete_policy_returns_not_found(self): + self.assertRaises(exception.PolicyNotFound, + self.policy_api.delete_policy, + uuid.uuid4().hex) diff --git a/keystone-moon/keystone/tests/unit/resource/__init__.py b/keystone-moon/keystone/tests/unit/resource/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/unit/resource/backends/__init__.py b/keystone-moon/keystone/tests/unit/resource/backends/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/unit/resource/backends/test_sql.py b/keystone-moon/keystone/tests/unit/resource/backends/test_sql.py new file mode 100644 index 00000000..79ad3df2 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/resource/backends/test_sql.py @@ -0,0 +1,24 @@ +# 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.resource.backends import sql +from keystone.tests import unit +from keystone.tests.unit.ksfixtures import database +from keystone.tests.unit.resource import test_backends + + +class TestSqlResourceDriver(unit.BaseTestCase, + test_backends.ResourceDriverTests): + def setUp(self): + super(TestSqlResourceDriver, self).setUp() + self.useFixture(database.Database()) + self.driver = sql.Resource() diff --git a/keystone-moon/keystone/tests/unit/resource/config_backends/__init__.py b/keystone-moon/keystone/tests/unit/resource/config_backends/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/unit/resource/config_backends/test_sql.py b/keystone-moon/keystone/tests/unit/resource/config_backends/test_sql.py new file mode 100644 index 00000000..b4c5f262 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/resource/config_backends/test_sql.py @@ -0,0 +1,53 @@ +# 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 sql +from keystone.resource.config_backends import sql as config_sql +from keystone.tests import unit +from keystone.tests.unit.backend import core_sql +from keystone.tests.unit.ksfixtures import database +from keystone.tests.unit.resource import test_core + + +class SqlDomainConfigModels(core_sql.BaseBackendSqlModels): + + def test_whitelisted_model(self): + cols = (('domain_id', sql.String, 64), + ('group', sql.String, 255), + ('option', sql.String, 255), + ('value', sql.JsonBlob, None)) + self.assertExpectedSchema('whitelisted_config', cols) + + def test_sensitive_model(self): + cols = (('domain_id', sql.String, 64), + ('group', sql.String, 255), + ('option', sql.String, 255), + ('value', sql.JsonBlob, None)) + self.assertExpectedSchema('sensitive_config', cols) + + +class SqlDomainConfigDriver(unit.BaseTestCase, + test_core.DomainConfigDriverTests): + def setUp(self): + super(SqlDomainConfigDriver, self).setUp() + self.useFixture(database.Database()) + self.driver = config_sql.DomainConfig() + + +class SqlDomainConfig(core_sql.BaseBackendSqlTests, + test_core.DomainConfigTests): + def setUp(self): + super(SqlDomainConfig, self).setUp() + # test_core.DomainConfigTests is effectively a mixin class, so make + # sure we call its setup + test_core.DomainConfigTests.setUp(self) diff --git a/keystone-moon/keystone/tests/unit/resource/test_backends.py b/keystone-moon/keystone/tests/unit/resource/test_backends.py new file mode 100644 index 00000000..eed4c6ba --- /dev/null +++ b/keystone-moon/keystone/tests/unit/resource/test_backends.py @@ -0,0 +1,1669 @@ +# 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 copy +import uuid + +import mock +from oslo_config import cfg +from six.moves import range +from testtools import matchers + +from keystone.common import driver_hints +from keystone import exception +from keystone.tests import unit +from keystone.tests.unit import default_fixtures +from keystone.tests.unit import utils as test_utils + + +CONF = cfg.CONF + + +class ResourceTests(object): + + domain_count = len(default_fixtures.DOMAINS) + + def test_get_project(self): + tenant_ref = self.resource_api.get_project(self.tenant_bar['id']) + self.assertDictEqual(self.tenant_bar, tenant_ref) + + def test_get_project_returns_not_found(self): + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + uuid.uuid4().hex) + + def test_get_project_by_name(self): + tenant_ref = self.resource_api.get_project_by_name( + self.tenant_bar['name'], + CONF.identity.default_domain_id) + self.assertDictEqual(self.tenant_bar, tenant_ref) + + @unit.skip_if_no_multiple_domains_support + def test_get_project_by_name_for_project_acting_as_a_domain(self): + """Tests get_project_by_name works when the domain_id is None.""" + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id, is_domain=False) + project = self.resource_api.create_project(project['id'], project) + + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project_by_name, + project['name'], + None) + + # Test that querying with domain_id as None will find the project + # acting as a domain, even if it's name is the same as the regular + # project above. + project2 = unit.new_project_ref(is_domain=True, + name=project['name']) + project2 = self.resource_api.create_project(project2['id'], project2) + + project_ref = self.resource_api.get_project_by_name( + project2['name'], None) + + self.assertEqual(project2, project_ref) + + def test_get_project_by_name_returns_not_found(self): + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project_by_name, + uuid.uuid4().hex, + CONF.identity.default_domain_id) + + def test_create_duplicate_project_id_fails(self): + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + project_id = project['id'] + self.resource_api.create_project(project_id, project) + project['name'] = 'fake2' + self.assertRaises(exception.Conflict, + self.resource_api.create_project, + project_id, + project) + + def test_create_duplicate_project_name_fails(self): + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + project_id = project['id'] + self.resource_api.create_project(project_id, project) + project['id'] = 'fake2' + self.assertRaises(exception.Conflict, + self.resource_api.create_project, + project['id'], + project) + + def test_create_duplicate_project_name_in_different_domains(self): + new_domain = unit.new_domain_ref() + self.resource_api.create_domain(new_domain['id'], new_domain) + project1 = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + project2 = unit.new_project_ref(name=project1['name'], + domain_id=new_domain['id']) + self.resource_api.create_project(project1['id'], project1) + self.resource_api.create_project(project2['id'], project2) + + def test_move_project_between_domains(self): + domain1 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = unit.new_domain_ref() + self.resource_api.create_domain(domain2['id'], domain2) + project = unit.new_project_ref(domain_id=domain1['id']) + self.resource_api.create_project(project['id'], project) + project['domain_id'] = domain2['id'] + # Update the project asserting that a deprecation warning is emitted + with mock.patch( + 'oslo_log.versionutils.report_deprecated_feature') as mock_dep: + self.resource_api.update_project(project['id'], project) + self.assertTrue(mock_dep.called) + + updated_project_ref = self.resource_api.get_project(project['id']) + self.assertEqual(domain2['id'], updated_project_ref['domain_id']) + + def test_move_project_between_domains_with_clashing_names_fails(self): + domain1 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = unit.new_domain_ref() + self.resource_api.create_domain(domain2['id'], domain2) + # First, create a project in domain1 + project1 = unit.new_project_ref(domain_id=domain1['id']) + self.resource_api.create_project(project1['id'], project1) + # Now create a project in domain2 with a potentially clashing + # name - which should work since we have domain separation + project2 = unit.new_project_ref(name=project1['name'], + domain_id=domain2['id']) + self.resource_api.create_project(project2['id'], project2) + # Now try and move project1 into the 2nd domain - which should + # fail since the names clash + project1['domain_id'] = domain2['id'] + self.assertRaises(exception.Conflict, + self.resource_api.update_project, + project1['id'], + project1) + + @unit.skip_if_no_multiple_domains_support + def test_move_project_with_children_between_domains_fails(self): + domain1 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = unit.new_domain_ref() + self.resource_api.create_domain(domain2['id'], domain2) + project = unit.new_project_ref(domain_id=domain1['id']) + self.resource_api.create_project(project['id'], project) + child_project = unit.new_project_ref(domain_id=domain1['id'], + parent_id=project['id']) + self.resource_api.create_project(child_project['id'], child_project) + project['domain_id'] = domain2['id'] + + # Update is not allowed, since updating the whole subtree would be + # necessary + self.assertRaises(exception.ValidationError, + self.resource_api.update_project, + project['id'], + project) + + @unit.skip_if_no_multiple_domains_support + def test_move_project_not_root_between_domains_fails(self): + domain1 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = unit.new_domain_ref() + self.resource_api.create_domain(domain2['id'], domain2) + project = unit.new_project_ref(domain_id=domain1['id']) + self.resource_api.create_project(project['id'], project) + child_project = unit.new_project_ref(domain_id=domain1['id'], + parent_id=project['id']) + self.resource_api.create_project(child_project['id'], child_project) + child_project['domain_id'] = domain2['id'] + + self.assertRaises(exception.ValidationError, + self.resource_api.update_project, + child_project['id'], + child_project) + + @unit.skip_if_no_multiple_domains_support + def test_move_root_project_between_domains_succeeds(self): + domain1 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = unit.new_domain_ref() + self.resource_api.create_domain(domain2['id'], domain2) + root_project = unit.new_project_ref(domain_id=domain1['id']) + root_project = self.resource_api.create_project(root_project['id'], + root_project) + + root_project['domain_id'] = domain2['id'] + self.resource_api.update_project(root_project['id'], root_project) + project_from_db = self.resource_api.get_project(root_project['id']) + + self.assertEqual(domain2['id'], project_from_db['domain_id']) + + @unit.skip_if_no_multiple_domains_support + def test_update_domain_id_project_is_domain_fails(self): + other_domain = unit.new_domain_ref() + self.resource_api.create_domain(other_domain['id'], other_domain) + project = unit.new_project_ref(is_domain=True) + self.resource_api.create_project(project['id'], project) + project['domain_id'] = other_domain['id'] + + # Update of domain_id of projects acting as domains is not allowed + self.assertRaises(exception.ValidationError, + self.resource_api.update_project, + project['id'], + project) + + def test_rename_duplicate_project_name_fails(self): + project1 = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + project2 = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + self.resource_api.create_project(project1['id'], project1) + self.resource_api.create_project(project2['id'], project2) + project2['name'] = project1['name'] + self.assertRaises(exception.Error, + self.resource_api.update_project, + project2['id'], + project2) + + def test_update_project_id_does_nothing(self): + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + project_id = project['id'] + self.resource_api.create_project(project['id'], project) + project['id'] = 'fake2' + self.resource_api.update_project(project_id, project) + project_ref = self.resource_api.get_project(project_id) + self.assertEqual(project_id, project_ref['id']) + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + 'fake2') + + def test_delete_domain_with_user_group_project_links(self): + # TODO(chungg):add test case once expected behaviour defined + pass + + def test_update_project_returns_not_found(self): + self.assertRaises(exception.ProjectNotFound, + self.resource_api.update_project, + uuid.uuid4().hex, + dict()) + + def test_delete_project_returns_not_found(self): + self.assertRaises(exception.ProjectNotFound, + self.resource_api.delete_project, + uuid.uuid4().hex) + + def test_create_update_delete_unicode_project(self): + unicode_project_name = u'name \u540d\u5b57' + project = unit.new_project_ref( + name=unicode_project_name, + domain_id=CONF.identity.default_domain_id) + project = self.resource_api.create_project(project['id'], project) + self.resource_api.update_project(project['id'], project) + self.resource_api.delete_project(project['id']) + + def test_create_project_with_no_enabled_field(self): + ref = unit.new_project_ref(domain_id=CONF.identity.default_domain_id) + del ref['enabled'] + self.resource_api.create_project(ref['id'], ref) + + project = self.resource_api.get_project(ref['id']) + self.assertIs(project['enabled'], True) + + def test_create_project_long_name_fails(self): + project = unit.new_project_ref( + name='a' * 65, domain_id=CONF.identity.default_domain_id) + self.assertRaises(exception.ValidationError, + self.resource_api.create_project, + project['id'], + project) + + def test_create_project_blank_name_fails(self): + project = unit.new_project_ref( + name='', domain_id=CONF.identity.default_domain_id) + self.assertRaises(exception.ValidationError, + self.resource_api.create_project, + project['id'], + project) + + def test_create_project_invalid_name_fails(self): + project = unit.new_project_ref( + name=None, domain_id=CONF.identity.default_domain_id) + self.assertRaises(exception.ValidationError, + self.resource_api.create_project, + project['id'], + project) + project = unit.new_project_ref( + name=123, domain_id=CONF.identity.default_domain_id) + self.assertRaises(exception.ValidationError, + self.resource_api.create_project, + project['id'], + project) + + def test_update_project_blank_name_fails(self): + project = unit.new_project_ref( + name='fake1', domain_id=CONF.identity.default_domain_id) + self.resource_api.create_project(project['id'], project) + project['name'] = '' + self.assertRaises(exception.ValidationError, + self.resource_api.update_project, + project['id'], + project) + + def test_update_project_long_name_fails(self): + project = unit.new_project_ref( + name='fake1', domain_id=CONF.identity.default_domain_id) + self.resource_api.create_project(project['id'], project) + project['name'] = 'a' * 65 + self.assertRaises(exception.ValidationError, + self.resource_api.update_project, + project['id'], + project) + + def test_update_project_invalid_name_fails(self): + project = unit.new_project_ref( + name='fake1', domain_id=CONF.identity.default_domain_id) + self.resource_api.create_project(project['id'], project) + project['name'] = None + self.assertRaises(exception.ValidationError, + self.resource_api.update_project, + project['id'], + project) + + project['name'] = 123 + self.assertRaises(exception.ValidationError, + self.resource_api.update_project, + project['id'], + project) + + def test_update_project_invalid_enabled_type_string(self): + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + self.resource_api.create_project(project['id'], project) + project_ref = self.resource_api.get_project(project['id']) + self.assertTrue(project_ref['enabled']) + + # Strings are not valid boolean values + project['enabled'] = "false" + self.assertRaises(exception.ValidationError, + self.resource_api.update_project, + project['id'], + project) + + def test_create_project_invalid_enabled_type_string(self): + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id, + # invalid string value + enabled="true") + self.assertRaises(exception.ValidationError, + self.resource_api.create_project, + project['id'], + project) + + def test_create_project_invalid_domain_id(self): + project = unit.new_project_ref(domain_id=uuid.uuid4().hex) + self.assertRaises(exception.DomainNotFound, + self.resource_api.create_project, + project['id'], + project) + + def test_list_domains(self): + domain1 = unit.new_domain_ref() + domain2 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + self.resource_api.create_domain(domain2['id'], domain2) + domains = self.resource_api.list_domains() + self.assertEqual(3, len(domains)) + domain_ids = [] + for domain in domains: + domain_ids.append(domain.get('id')) + self.assertIn(CONF.identity.default_domain_id, domain_ids) + self.assertIn(domain1['id'], domain_ids) + self.assertIn(domain2['id'], domain_ids) + + def test_list_projects(self): + project_refs = self.resource_api.list_projects() + project_count = len(default_fixtures.TENANTS) + self.domain_count + self.assertEqual(project_count, len(project_refs)) + for project in default_fixtures.TENANTS: + self.assertIn(project, project_refs) + + def test_list_projects_with_multiple_filters(self): + # Create a project + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + project = self.resource_api.create_project(project['id'], project) + + # Build driver hints with the project's name and inexistent description + hints = driver_hints.Hints() + hints.add_filter('name', project['name']) + hints.add_filter('description', uuid.uuid4().hex) + + # Retrieve projects based on hints and check an empty list is returned + projects = self.resource_api.list_projects(hints) + self.assertEqual([], projects) + + # Build correct driver hints + hints = driver_hints.Hints() + hints.add_filter('name', project['name']) + hints.add_filter('description', project['description']) + + # Retrieve projects based on hints + projects = self.resource_api.list_projects(hints) + + # Check that the returned list contains only the first project + self.assertEqual(1, len(projects)) + self.assertEqual(project, projects[0]) + + def test_list_projects_for_domain(self): + project_ids = ([x['id'] for x in + self.resource_api.list_projects_in_domain( + CONF.identity.default_domain_id)]) + # Only the projects from the default fixtures are expected, since + # filtering by domain does not include any project that acts as a + # domain. + self.assertThat( + project_ids, matchers.HasLength(len(default_fixtures.TENANTS))) + self.assertIn(self.tenant_bar['id'], project_ids) + self.assertIn(self.tenant_baz['id'], project_ids) + self.assertIn(self.tenant_mtu['id'], project_ids) + self.assertIn(self.tenant_service['id'], project_ids) + + @unit.skip_if_no_multiple_domains_support + def test_list_projects_acting_as_domain(self): + initial_domains = self.resource_api.list_domains() + + # Creating 5 projects that act as domains + new_projects_acting_as_domains = [] + for i in range(5): + project = unit.new_project_ref(is_domain=True) + project = self.resource_api.create_project(project['id'], project) + new_projects_acting_as_domains.append(project) + + # Creating a few regular project to ensure it doesn't mess with the + # ones that act as domains + self._create_projects_hierarchy(hierarchy_size=2) + + projects = self.resource_api.list_projects_acting_as_domain() + expected_number_projects = ( + len(initial_domains) + len(new_projects_acting_as_domains)) + self.assertEqual(expected_number_projects, len(projects)) + for project in new_projects_acting_as_domains: + self.assertIn(project, projects) + for domain in initial_domains: + self.assertIn(domain['id'], [p['id'] for p in projects]) + + @unit.skip_if_no_multiple_domains_support + def test_list_projects_for_alternate_domain(self): + domain1 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + project1 = unit.new_project_ref(domain_id=domain1['id']) + self.resource_api.create_project(project1['id'], project1) + project2 = unit.new_project_ref(domain_id=domain1['id']) + self.resource_api.create_project(project2['id'], project2) + project_ids = ([x['id'] for x in + self.resource_api.list_projects_in_domain( + domain1['id'])]) + self.assertEqual(2, len(project_ids)) + self.assertIn(project1['id'], project_ids) + self.assertIn(project2['id'], project_ids) + + def _create_projects_hierarchy(self, hierarchy_size=2, + domain_id=None, + is_domain=False, + parent_project_id=None): + """Creates a project hierarchy with specified size. + + :param hierarchy_size: the desired hierarchy size, default is 2 - + a project with one child. + :param domain_id: domain where the projects hierarchy will be created. + :param is_domain: if the hierarchy will have the is_domain flag active + or not. + :param parent_project_id: if the intention is to create a + sub-hierarchy, sets the sub-hierarchy root. Defaults to creating + a new hierarchy, i.e. a new root project. + + :returns projects: a list of the projects in the created hierarchy. + + """ + if domain_id is None: + domain_id = CONF.identity.default_domain_id + if parent_project_id: + project = unit.new_project_ref(parent_id=parent_project_id, + domain_id=domain_id, + is_domain=is_domain) + else: + project = unit.new_project_ref(domain_id=domain_id, + is_domain=is_domain) + project_id = project['id'] + project = self.resource_api.create_project(project_id, project) + + projects = [project] + for i in range(1, hierarchy_size): + new_project = unit.new_project_ref(parent_id=project_id, + domain_id=domain_id) + + self.resource_api.create_project(new_project['id'], new_project) + projects.append(new_project) + project_id = new_project['id'] + + return projects + + @unit.skip_if_no_multiple_domains_support + def test_create_domain_with_project_api(self): + project = unit.new_project_ref(is_domain=True) + ref = self.resource_api.create_project(project['id'], project) + self.assertTrue(ref['is_domain']) + self.resource_api.get_domain(ref['id']) + + @unit.skip_if_no_multiple_domains_support + def test_project_as_a_domain_uniqueness_constraints(self): + """Tests project uniqueness for those acting as domains. + + If it is a project acting as a domain, we can't have two or more with + the same name. + + """ + # Create two projects acting as a domain + project = unit.new_project_ref(is_domain=True) + project = self.resource_api.create_project(project['id'], project) + project2 = unit.new_project_ref(is_domain=True) + project2 = self.resource_api.create_project(project2['id'], project2) + + # All projects acting as domains have a null domain_id, so should not + # be able to create another with the same name but a different + # project ID. + new_project = project.copy() + new_project['id'] = uuid.uuid4().hex + + self.assertRaises(exception.Conflict, + self.resource_api.create_project, + new_project['id'], + new_project) + + # We also should not be able to update one to have a name clash + project2['name'] = project['name'] + self.assertRaises(exception.Conflict, + self.resource_api.update_project, + project2['id'], + project2) + + # But updating it to a unique name is OK + project2['name'] = uuid.uuid4().hex + self.resource_api.update_project(project2['id'], project2) + + # Finally, it should be OK to create a project with same name as one of + # these acting as a domain, as long as it is a regular project + project3 = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id, name=project2['name']) + self.resource_api.create_project(project3['id'], project3) + # In fact, it should be OK to create such a project in the domain which + # has the matching name. + # TODO(henry-nash): Once we fully support projects acting as a domain, + # add a test here to create a sub-project with a name that matches its + # project acting as a domain + + @unit.skip_if_no_multiple_domains_support + @test_utils.wip('waiting for sub projects acting as domains support') + def test_is_domain_sub_project_has_parent_domain_id(self): + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id, is_domain=True) + self.resource_api.create_project(project['id'], project) + + sub_project = unit.new_project_ref(domain_id=project['id'], + parent_id=project['id'], + is_domain=True) + + ref = self.resource_api.create_project(sub_project['id'], sub_project) + self.assertTrue(ref['is_domain']) + self.assertEqual(project['id'], ref['parent_id']) + self.assertEqual(project['id'], ref['domain_id']) + + @unit.skip_if_no_multiple_domains_support + def test_delete_domain_with_project_api(self): + project = unit.new_project_ref(domain_id=None, + is_domain=True) + self.resource_api.create_project(project['id'], project) + + # Check that a corresponding domain was created + self.resource_api.get_domain(project['id']) + + # Try to delete the enabled project that acts as a domain + self.assertRaises(exception.ForbiddenNotSecurity, + self.resource_api.delete_project, + project['id']) + + # Disable the project + project['enabled'] = False + self.resource_api.update_project(project['id'], project) + + # Successfully delete the project + self.resource_api.delete_project(project['id']) + + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + project['id']) + + self.assertRaises(exception.DomainNotFound, + self.resource_api.get_domain, + project['id']) + + @unit.skip_if_no_multiple_domains_support + def test_create_subproject_acting_as_domain_fails(self): + root_project = unit.new_project_ref(is_domain=True) + self.resource_api.create_project(root_project['id'], root_project) + + sub_project = unit.new_project_ref(is_domain=True, + parent_id=root_project['id']) + + # Creation of sub projects acting as domains is not allowed yet + self.assertRaises(exception.ValidationError, + self.resource_api.create_project, + sub_project['id'], sub_project) + + @unit.skip_if_no_multiple_domains_support + def test_create_domain_under_regular_project_hierarchy_fails(self): + # Projects acting as domains can't have a regular project as parent + projects_hierarchy = self._create_projects_hierarchy() + parent = projects_hierarchy[1] + project = unit.new_project_ref(domain_id=parent['id'], + parent_id=parent['id'], + is_domain=True) + + self.assertRaises(exception.ValidationError, + self.resource_api.create_project, + project['id'], project) + + @unit.skip_if_no_multiple_domains_support + @test_utils.wip('waiting for sub projects acting as domains support') + def test_create_project_under_domain_hierarchy(self): + projects_hierarchy = self._create_projects_hierarchy(is_domain=True) + parent = projects_hierarchy[1] + project = unit.new_project_ref(domain_id=parent['id'], + parent_id=parent['id'], + is_domain=False) + + ref = self.resource_api.create_project(project['id'], project) + self.assertFalse(ref['is_domain']) + self.assertEqual(parent['id'], ref['parent_id']) + self.assertEqual(parent['id'], ref['domain_id']) + + def test_create_project_without_is_domain_flag(self): + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + del project['is_domain'] + ref = self.resource_api.create_project(project['id'], project) + # The is_domain flag should be False by default + self.assertFalse(ref['is_domain']) + + @unit.skip_if_no_multiple_domains_support + def test_create_project_passing_is_domain_flag_true(self): + project = unit.new_project_ref(is_domain=True) + + ref = self.resource_api.create_project(project['id'], project) + self.assertTrue(ref['is_domain']) + + def test_create_project_passing_is_domain_flag_false(self): + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id, is_domain=False) + + ref = self.resource_api.create_project(project['id'], project) + self.assertIs(False, ref['is_domain']) + + @test_utils.wip('waiting for support for parent_id to imply domain_id') + def test_create_project_with_parent_id_and_without_domain_id(self): + # First create a domain + project = unit.new_project_ref(is_domain=True) + self.resource_api.create_project(project['id'], project) + # Now create a child by just naming the parent_id + sub_project = unit.new_project_ref(parent_id=project['id']) + ref = self.resource_api.create_project(sub_project['id'], sub_project) + + # The domain_id should be set to the parent domain_id + self.assertEqual(project['domain_id'], ref['domain_id']) + + def test_create_project_with_domain_id_and_without_parent_id(self): + # First create a domain + project = unit.new_project_ref(is_domain=True) + self.resource_api.create_project(project['id'], project) + # Now create a child by just naming the domain_id + sub_project = unit.new_project_ref(domain_id=project['id']) + ref = self.resource_api.create_project(sub_project['id'], sub_project) + + # The parent_id and domain_id should be set to the id of the project + # acting as a domain + self.assertEqual(project['id'], ref['parent_id']) + self.assertEqual(project['id'], ref['domain_id']) + + def test_create_project_with_domain_id_mismatch_to_parent_domain(self): + # First create a domain + project = unit.new_project_ref(is_domain=True) + self.resource_api.create_project(project['id'], project) + # Now try to create a child with the above as its parent, but + # specifying a different domain. + sub_project = unit.new_project_ref( + parent_id=project['id'], domain_id=CONF.identity.default_domain_id) + self.assertRaises(exception.ValidationError, + self.resource_api.create_project, + sub_project['id'], sub_project) + + def test_check_leaf_projects(self): + projects_hierarchy = self._create_projects_hierarchy() + root_project = projects_hierarchy[0] + leaf_project = projects_hierarchy[1] + + self.assertFalse(self.resource_api.is_leaf_project( + root_project['id'])) + self.assertTrue(self.resource_api.is_leaf_project( + leaf_project['id'])) + + # Delete leaf_project + self.resource_api.delete_project(leaf_project['id']) + + # Now, root_project should be leaf + self.assertTrue(self.resource_api.is_leaf_project( + root_project['id'])) + + def test_list_projects_in_subtree(self): + projects_hierarchy = self._create_projects_hierarchy(hierarchy_size=3) + project1 = projects_hierarchy[0] + project2 = projects_hierarchy[1] + project3 = projects_hierarchy[2] + project4 = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id, + parent_id=project2['id']) + self.resource_api.create_project(project4['id'], project4) + + subtree = self.resource_api.list_projects_in_subtree(project1['id']) + self.assertEqual(3, len(subtree)) + self.assertIn(project2, subtree) + self.assertIn(project3, subtree) + self.assertIn(project4, subtree) + + subtree = self.resource_api.list_projects_in_subtree(project2['id']) + self.assertEqual(2, len(subtree)) + self.assertIn(project3, subtree) + self.assertIn(project4, subtree) + + subtree = self.resource_api.list_projects_in_subtree(project3['id']) + self.assertEqual(0, len(subtree)) + + def test_list_projects_in_subtree_with_circular_reference(self): + project1 = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + project1 = self.resource_api.create_project(project1['id'], project1) + + project2 = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id, + parent_id=project1['id']) + self.resource_api.create_project(project2['id'], project2) + + project1['parent_id'] = project2['id'] # Adds cyclic reference + + # NOTE(dstanek): The manager does not allow parent_id to be updated. + # Instead will directly use the driver to create the cyclic + # reference. + self.resource_api.driver.update_project(project1['id'], project1) + + subtree = self.resource_api.list_projects_in_subtree(project1['id']) + + # NOTE(dstanek): If a cyclic reference is detected the code bails + # and returns None instead of falling into the infinite + # recursion trap. + self.assertIsNone(subtree) + + def test_list_projects_in_subtree_invalid_project_id(self): + self.assertRaises(exception.ValidationError, + self.resource_api.list_projects_in_subtree, + None) + + self.assertRaises(exception.ProjectNotFound, + self.resource_api.list_projects_in_subtree, + uuid.uuid4().hex) + + def test_list_project_parents(self): + projects_hierarchy = self._create_projects_hierarchy(hierarchy_size=3) + project1 = projects_hierarchy[0] + project2 = projects_hierarchy[1] + project3 = projects_hierarchy[2] + project4 = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id, + parent_id=project2['id']) + self.resource_api.create_project(project4['id'], project4) + + parents1 = self.resource_api.list_project_parents(project3['id']) + self.assertEqual(3, len(parents1)) + self.assertIn(project1, parents1) + self.assertIn(project2, parents1) + + parents2 = self.resource_api.list_project_parents(project4['id']) + self.assertEqual(parents1, parents2) + + parents = self.resource_api.list_project_parents(project1['id']) + # It has the default domain as parent + self.assertEqual(1, len(parents)) + + def test_update_project_enabled_cascade(self): + """Test update_project_cascade + + Ensures the enabled attribute is correctly updated across + a simple 3-level projects hierarchy. + """ + projects_hierarchy = self._create_projects_hierarchy(hierarchy_size=3) + parent = projects_hierarchy[0] + + # Disable in parent project disables the whole subtree + parent['enabled'] = False + # Store the ref from backend in another variable so we don't bother + # to remove other attributes that were not originally provided and + # were set in the manager, like parent_id and domain_id. + parent_ref = self.resource_api.update_project(parent['id'], + parent, + cascade=True) + + subtree = self.resource_api.list_projects_in_subtree(parent['id']) + self.assertEqual(2, len(subtree)) + self.assertFalse(parent_ref['enabled']) + self.assertFalse(subtree[0]['enabled']) + self.assertFalse(subtree[1]['enabled']) + + # Enable parent project enables the whole subtree + parent['enabled'] = True + parent_ref = self.resource_api.update_project(parent['id'], + parent, + cascade=True) + + subtree = self.resource_api.list_projects_in_subtree(parent['id']) + self.assertEqual(2, len(subtree)) + self.assertTrue(parent_ref['enabled']) + self.assertTrue(subtree[0]['enabled']) + self.assertTrue(subtree[1]['enabled']) + + def test_cannot_enable_cascade_with_parent_disabled(self): + projects_hierarchy = self._create_projects_hierarchy(hierarchy_size=3) + grandparent = projects_hierarchy[0] + parent = projects_hierarchy[1] + + grandparent['enabled'] = False + self.resource_api.update_project(grandparent['id'], + grandparent, + cascade=True) + subtree = self.resource_api.list_projects_in_subtree(parent['id']) + self.assertFalse(subtree[0]['enabled']) + + parent['enabled'] = True + self.assertRaises(exception.ForbiddenNotSecurity, + self.resource_api.update_project, + parent['id'], + parent, + cascade=True) + + def test_update_cascade_only_accepts_enabled(self): + # Update cascade does not accept any other attribute but 'enabled' + new_project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + self.resource_api.create_project(new_project['id'], new_project) + + new_project['name'] = 'project1' + self.assertRaises(exception.ValidationError, + self.resource_api.update_project, + new_project['id'], + new_project, + cascade=True) + + def test_list_project_parents_invalid_project_id(self): + self.assertRaises(exception.ValidationError, + self.resource_api.list_project_parents, + None) + + self.assertRaises(exception.ProjectNotFound, + self.resource_api.list_project_parents, + uuid.uuid4().hex) + + def test_create_project_doesnt_modify_passed_in_dict(self): + new_project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + original_project = new_project.copy() + self.resource_api.create_project(new_project['id'], new_project) + self.assertDictEqual(original_project, new_project) + + def test_update_project_enable(self): + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + self.resource_api.create_project(project['id'], project) + project_ref = self.resource_api.get_project(project['id']) + self.assertTrue(project_ref['enabled']) + + project['enabled'] = False + self.resource_api.update_project(project['id'], project) + project_ref = self.resource_api.get_project(project['id']) + self.assertEqual(project['enabled'], project_ref['enabled']) + + # If not present, enabled field should not be updated + del project['enabled'] + self.resource_api.update_project(project['id'], project) + project_ref = self.resource_api.get_project(project['id']) + self.assertFalse(project_ref['enabled']) + + project['enabled'] = True + self.resource_api.update_project(project['id'], project) + project_ref = self.resource_api.get_project(project['id']) + self.assertEqual(project['enabled'], project_ref['enabled']) + + del project['enabled'] + self.resource_api.update_project(project['id'], project) + project_ref = self.resource_api.get_project(project['id']) + self.assertTrue(project_ref['enabled']) + + def test_create_invalid_domain_fails(self): + new_group = unit.new_group_ref(domain_id="doesnotexist") + self.assertRaises(exception.DomainNotFound, + self.identity_api.create_group, + new_group) + new_user = unit.new_user_ref(domain_id="doesnotexist") + self.assertRaises(exception.DomainNotFound, + self.identity_api.create_user, + new_user) + + @unit.skip_if_no_multiple_domains_support + def test_project_crud(self): + domain = unit.new_domain_ref() + self.resource_api.create_domain(domain['id'], domain) + project = unit.new_project_ref(domain_id=domain['id']) + self.resource_api.create_project(project['id'], project) + project_ref = self.resource_api.get_project(project['id']) + self.assertDictContainsSubset(project, project_ref) + + project['name'] = uuid.uuid4().hex + self.resource_api.update_project(project['id'], project) + project_ref = self.resource_api.get_project(project['id']) + self.assertDictContainsSubset(project, project_ref) + + self.resource_api.delete_project(project['id']) + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + project['id']) + + def test_domain_delete_hierarchy(self): + domain = unit.new_domain_ref() + self.resource_api.create_domain(domain['id'], domain) + + # Creating a root and a leaf project inside the domain + projects_hierarchy = self._create_projects_hierarchy( + domain_id=domain['id']) + root_project = projects_hierarchy[0] + leaf_project = projects_hierarchy[0] + + # Disable the domain + domain['enabled'] = False + self.resource_api.update_domain(domain['id'], domain) + + # Delete the domain + self.resource_api.delete_domain(domain['id']) + + # Make sure the domain no longer exists + self.assertRaises(exception.DomainNotFound, + self.resource_api.get_domain, + domain['id']) + + # Make sure the root project no longer exists + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + root_project['id']) + + # Make sure the leaf project no longer exists + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + leaf_project['id']) + + def test_delete_projects_from_ids(self): + """Tests the resource backend call delete_projects_from_ids. + + Tests the normal flow of the delete_projects_from_ids backend call, + that ensures no project on the list exists after it is succesfully + called. + """ + project1_ref = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + project2_ref = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + projects = (project1_ref, project2_ref) + for project in projects: + self.resource_api.create_project(project['id'], project) + + # Setting up the ID's list + projects_ids = [p['id'] for p in projects] + self.resource_api.driver.delete_projects_from_ids(projects_ids) + + # Ensuring projects no longer exist at backend level + for project_id in projects_ids: + self.assertRaises(exception.ProjectNotFound, + self.resource_api.driver.get_project, + project_id) + + # Passing an empty list is silently ignored + self.resource_api.driver.delete_projects_from_ids([]) + + def test_delete_projects_from_ids_with_no_existing_project_id(self): + """Tests delete_projects_from_ids issues warning if not found. + + Tests the resource backend call delete_projects_from_ids passing a + non existing ID in project_ids, which is logged and ignored by + the backend. + """ + project_ref = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + self.resource_api.create_project(project_ref['id'], project_ref) + + # Setting up the ID's list + projects_ids = (project_ref['id'], uuid.uuid4().hex) + with mock.patch('keystone.resource.backends.sql.LOG') as mock_log: + self.resource_api.delete_projects_from_ids(projects_ids) + self.assertTrue(mock_log.warning.called) + # The existing project was deleted. + self.assertRaises(exception.ProjectNotFound, + self.resource_api.driver.get_project, + project_ref['id']) + + # Even if we only have one project, and it does not exist, it returns + # no error. + self.resource_api.driver.delete_projects_from_ids([uuid.uuid4().hex]) + + def test_delete_project_cascade(self): + # create a hierarchy with 3 levels + projects_hierarchy = self._create_projects_hierarchy(hierarchy_size=3) + root_project = projects_hierarchy[0] + project1 = projects_hierarchy[1] + project2 = projects_hierarchy[2] + + # Disabling all projects before attempting to delete + for project in (project2, project1, root_project): + project['enabled'] = False + self.resource_api.update_project(project['id'], project) + + self.resource_api.delete_project(root_project['id'], cascade=True) + + for project in projects_hierarchy: + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + project['id']) + + def test_delete_large_project_cascade(self): + """Try delete a large project with cascade true. + + Tree we will create:: + + +-p1-+ + | | + p5 p2 + | | + p6 +-p3-+ + | | + p7 p4 + """ + # create a hierarchy with 4 levels + projects_hierarchy = self._create_projects_hierarchy(hierarchy_size=4) + p1 = projects_hierarchy[0] + # Add the left branch to the hierarchy (p5, p6) + self._create_projects_hierarchy(hierarchy_size=2, + parent_project_id=p1['id']) + # Add p7 to the hierarchy + p3_id = projects_hierarchy[2]['id'] + self._create_projects_hierarchy(hierarchy_size=1, + parent_project_id=p3_id) + # Reverse the hierarchy to disable the leaf first + prjs_hierarchy = ([p1] + self.resource_api.list_projects_in_subtree( + p1['id']))[::-1] + + # Disabling all projects before attempting to delete + for project in prjs_hierarchy: + project['enabled'] = False + self.resource_api.update_project(project['id'], project) + + self.resource_api.delete_project(p1['id'], cascade=True) + for project in prjs_hierarchy: + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + project['id']) + + def test_cannot_delete_project_cascade_with_enabled_child(self): + # create a hierarchy with 3 levels + projects_hierarchy = self._create_projects_hierarchy(hierarchy_size=3) + root_project = projects_hierarchy[0] + project1 = projects_hierarchy[1] + project2 = projects_hierarchy[2] + + project2['enabled'] = False + self.resource_api.update_project(project2['id'], project2) + + # Cannot cascade delete root_project, since project1 is enabled + self.assertRaises(exception.ForbiddenNotSecurity, + self.resource_api.delete_project, + root_project['id'], + cascade=True) + + # Ensuring no project was deleted, not even project2 + self.resource_api.get_project(root_project['id']) + self.resource_api.get_project(project1['id']) + self.resource_api.get_project(project2['id']) + + def test_hierarchical_projects_crud(self): + # create a hierarchy with just a root project (which is a leaf as well) + projects_hierarchy = self._create_projects_hierarchy(hierarchy_size=1) + root_project1 = projects_hierarchy[0] + + # create a hierarchy with one root project and one leaf project + projects_hierarchy = self._create_projects_hierarchy() + root_project2 = projects_hierarchy[0] + leaf_project = projects_hierarchy[1] + + # update description from leaf_project + leaf_project['description'] = 'new description' + self.resource_api.update_project(leaf_project['id'], leaf_project) + proj_ref = self.resource_api.get_project(leaf_project['id']) + self.assertDictEqual(leaf_project, proj_ref) + + # update the parent_id is not allowed + leaf_project['parent_id'] = root_project1['id'] + self.assertRaises(exception.ForbiddenNotSecurity, + self.resource_api.update_project, + leaf_project['id'], + leaf_project) + + # delete root_project1 + self.resource_api.delete_project(root_project1['id']) + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + root_project1['id']) + + # delete root_project2 is not allowed since it is not a leaf project + self.assertRaises(exception.ForbiddenNotSecurity, + self.resource_api.delete_project, + root_project2['id']) + + def test_create_project_with_invalid_parent(self): + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id, parent_id='fake') + self.assertRaises(exception.ProjectNotFound, + self.resource_api.create_project, + project['id'], + project) + + @unit.skip_if_no_multiple_domains_support + def test_create_leaf_project_with_different_domain(self): + root_project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + self.resource_api.create_project(root_project['id'], root_project) + + domain = unit.new_domain_ref() + self.resource_api.create_domain(domain['id'], domain) + leaf_project = unit.new_project_ref(domain_id=domain['id'], + parent_id=root_project['id']) + + self.assertRaises(exception.ValidationError, + self.resource_api.create_project, + leaf_project['id'], + leaf_project) + + def test_delete_hierarchical_leaf_project(self): + projects_hierarchy = self._create_projects_hierarchy() + root_project = projects_hierarchy[0] + leaf_project = projects_hierarchy[1] + + self.resource_api.delete_project(leaf_project['id']) + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + leaf_project['id']) + + self.resource_api.delete_project(root_project['id']) + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + root_project['id']) + + def test_delete_hierarchical_not_leaf_project(self): + projects_hierarchy = self._create_projects_hierarchy() + root_project = projects_hierarchy[0] + + self.assertRaises(exception.ForbiddenNotSecurity, + self.resource_api.delete_project, + root_project['id']) + + def test_update_project_parent(self): + projects_hierarchy = self._create_projects_hierarchy(hierarchy_size=3) + project1 = projects_hierarchy[0] + project2 = projects_hierarchy[1] + project3 = projects_hierarchy[2] + + # project2 is the parent from project3 + self.assertEqual(project3.get('parent_id'), project2['id']) + + # try to update project3 parent to parent1 + project3['parent_id'] = project1['id'] + self.assertRaises(exception.ForbiddenNotSecurity, + self.resource_api.update_project, + project3['id'], + project3) + + def test_create_project_under_disabled_one(self): + project1 = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id, enabled=False) + self.resource_api.create_project(project1['id'], project1) + + project2 = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id, + parent_id=project1['id']) + + # It's not possible to create a project under a disabled one in the + # hierarchy + self.assertRaises(exception.ValidationError, + self.resource_api.create_project, + project2['id'], + project2) + + def test_disable_hierarchical_leaf_project(self): + projects_hierarchy = self._create_projects_hierarchy() + leaf_project = projects_hierarchy[1] + + leaf_project['enabled'] = False + self.resource_api.update_project(leaf_project['id'], leaf_project) + + project_ref = self.resource_api.get_project(leaf_project['id']) + self.assertEqual(leaf_project['enabled'], project_ref['enabled']) + + def test_disable_hierarchical_not_leaf_project(self): + projects_hierarchy = self._create_projects_hierarchy() + root_project = projects_hierarchy[0] + + root_project['enabled'] = False + self.assertRaises(exception.ForbiddenNotSecurity, + self.resource_api.update_project, + root_project['id'], + root_project) + + def test_enable_project_with_disabled_parent(self): + projects_hierarchy = self._create_projects_hierarchy() + root_project = projects_hierarchy[0] + leaf_project = projects_hierarchy[1] + + # Disable leaf and root + leaf_project['enabled'] = False + self.resource_api.update_project(leaf_project['id'], leaf_project) + root_project['enabled'] = False + self.resource_api.update_project(root_project['id'], root_project) + + # Try to enable the leaf project, it's not possible since it has + # a disabled parent + leaf_project['enabled'] = True + self.assertRaises(exception.ForbiddenNotSecurity, + self.resource_api.update_project, + leaf_project['id'], + leaf_project) + + def _get_hierarchy_depth(self, project_id): + return len(self.resource_api.list_project_parents(project_id)) + 1 + + def test_check_hierarchy_depth(self): + # Should be allowed to have a hierarchy of the max depth specified + # in the config option plus one (to allow for the additional project + # acting as a domain after an upgrade) + projects_hierarchy = self._create_projects_hierarchy( + CONF.max_project_tree_depth) + leaf_project = projects_hierarchy[CONF.max_project_tree_depth - 1] + + depth = self._get_hierarchy_depth(leaf_project['id']) + self.assertEqual(CONF.max_project_tree_depth + 1, depth) + + # Creating another project in the hierarchy shouldn't be allowed + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id, + parent_id=leaf_project['id']) + self.assertRaises(exception.ForbiddenNotSecurity, + self.resource_api.create_project, + project['id'], + project) + + def test_project_update_missing_attrs_with_a_value(self): + # Creating a project with no description attribute. + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + del project['description'] + project = self.resource_api.create_project(project['id'], project) + + # Add a description attribute. + project['description'] = uuid.uuid4().hex + self.resource_api.update_project(project['id'], project) + + project_ref = self.resource_api.get_project(project['id']) + self.assertDictEqual(project, project_ref) + + def test_project_update_missing_attrs_with_a_falsey_value(self): + # Creating a project with no description attribute. + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + del project['description'] + project = self.resource_api.create_project(project['id'], project) + + # Add a description attribute. + project['description'] = '' + self.resource_api.update_project(project['id'], project) + + project_ref = self.resource_api.get_project(project['id']) + self.assertDictEqual(project, project_ref) + + def test_domain_crud(self): + domain = unit.new_domain_ref() + domain_ref = self.resource_api.create_domain(domain['id'], domain) + self.assertDictEqual(domain, domain_ref) + domain_ref = self.resource_api.get_domain(domain['id']) + self.assertDictEqual(domain, domain_ref) + + domain['name'] = uuid.uuid4().hex + domain_ref = self.resource_api.update_domain(domain['id'], domain) + self.assertDictEqual(domain, domain_ref) + domain_ref = self.resource_api.get_domain(domain['id']) + self.assertDictEqual(domain, domain_ref) + + # Ensure an 'enabled' domain cannot be deleted + self.assertRaises(exception.ForbiddenNotSecurity, + self.resource_api.delete_domain, + domain_id=domain['id']) + + # Disable the domain + domain['enabled'] = False + self.resource_api.update_domain(domain['id'], domain) + + # Delete the domain + self.resource_api.delete_domain(domain['id']) + + # Make sure the domain no longer exists + self.assertRaises(exception.DomainNotFound, + self.resource_api.get_domain, + domain['id']) + + @unit.skip_if_no_multiple_domains_support + def test_domain_name_case_sensitivity(self): + # create a ref with a lowercase name + domain_name = 'test_domain' + ref = unit.new_domain_ref(name=domain_name) + + lower_case_domain = self.resource_api.create_domain(ref['id'], ref) + + # assign a new ID to the ref with the same name, but in uppercase + ref['id'] = uuid.uuid4().hex + ref['name'] = domain_name.upper() + upper_case_domain = self.resource_api.create_domain(ref['id'], ref) + + # We can get each domain by name + lower_case_domain_ref = self.resource_api.get_domain_by_name( + domain_name) + self.assertDictEqual(lower_case_domain, lower_case_domain_ref) + + upper_case_domain_ref = self.resource_api.get_domain_by_name( + domain_name.upper()) + self.assertDictEqual(upper_case_domain, upper_case_domain_ref) + + def test_project_attribute_update(self): + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + self.resource_api.create_project(project['id'], project) + + # pick a key known to be non-existent + key = 'description' + + def assert_key_equals(value): + project_ref = self.resource_api.update_project( + project['id'], project) + self.assertEqual(value, project_ref[key]) + project_ref = self.resource_api.get_project(project['id']) + self.assertEqual(value, project_ref[key]) + + def assert_get_key_is(value): + project_ref = self.resource_api.update_project( + project['id'], project) + self.assertIs(project_ref.get(key), value) + project_ref = self.resource_api.get_project(project['id']) + self.assertIs(project_ref.get(key), value) + + # add an attribute that doesn't exist, set it to a falsey value + value = '' + project[key] = value + assert_key_equals(value) + + # set an attribute with a falsey value to null + value = None + project[key] = value + assert_get_key_is(value) + + # do it again, in case updating from this situation is handled oddly + value = None + project[key] = value + assert_get_key_is(value) + + # set a possibly-null value to a falsey value + value = '' + project[key] = value + assert_key_equals(value) + + # set a falsey value to a truthy value + value = uuid.uuid4().hex + project[key] = value + assert_key_equals(value) + + @unit.skip_if_cache_disabled('resource') + @unit.skip_if_no_multiple_domains_support + def test_domain_rename_invalidates_get_domain_by_name_cache(self): + domain = unit.new_domain_ref() + domain_id = domain['id'] + domain_name = domain['name'] + self.resource_api.create_domain(domain_id, domain) + domain_ref = self.resource_api.get_domain_by_name(domain_name) + domain_ref['name'] = uuid.uuid4().hex + self.resource_api.update_domain(domain_id, domain_ref) + self.assertRaises(exception.DomainNotFound, + self.resource_api.get_domain_by_name, + domain_name) + + @unit.skip_if_cache_disabled('resource') + def test_cache_layer_domain_crud(self): + domain = unit.new_domain_ref() + domain_id = domain['id'] + # Create Domain + self.resource_api.create_domain(domain_id, domain) + project_domain_ref = self.resource_api.get_project(domain_id) + domain_ref = self.resource_api.get_domain(domain_id) + updated_project_domain_ref = copy.deepcopy(project_domain_ref) + updated_project_domain_ref['name'] = uuid.uuid4().hex + updated_domain_ref = copy.deepcopy(domain_ref) + updated_domain_ref['name'] = updated_project_domain_ref['name'] + # Update domain, bypassing resource api manager + self.resource_api.driver.update_project(domain_id, + updated_project_domain_ref) + # Verify get_domain still returns the domain + self.assertDictContainsSubset( + domain_ref, self.resource_api.get_domain(domain_id)) + # Invalidate cache + self.resource_api.get_domain.invalidate(self.resource_api, + domain_id) + # Verify get_domain returns the updated domain + self.assertDictContainsSubset( + updated_domain_ref, self.resource_api.get_domain(domain_id)) + # Update the domain back to original ref, using the assignment api + # manager + self.resource_api.update_domain(domain_id, domain_ref) + self.assertDictContainsSubset( + domain_ref, self.resource_api.get_domain(domain_id)) + # Make sure domain is 'disabled', bypass resource api manager + project_domain_ref_disabled = project_domain_ref.copy() + project_domain_ref_disabled['enabled'] = False + self.resource_api.driver.update_project(domain_id, + project_domain_ref_disabled) + self.resource_api.driver.update_project(domain_id, {'enabled': False}) + # Delete domain, bypassing resource api manager + self.resource_api.driver.delete_project(domain_id) + # Verify get_domain still returns the domain + self.assertDictContainsSubset( + domain_ref, self.resource_api.get_domain(domain_id)) + # Invalidate cache + self.resource_api.get_domain.invalidate(self.resource_api, + domain_id) + # Verify get_domain now raises DomainNotFound + self.assertRaises(exception.DomainNotFound, + self.resource_api.get_domain, domain_id) + # Recreate Domain + self.resource_api.create_domain(domain_id, domain) + self.resource_api.get_domain(domain_id) + # Make sure domain is 'disabled', bypass resource api manager + domain['enabled'] = False + self.resource_api.driver.update_project(domain_id, domain) + self.resource_api.driver.update_project(domain_id, {'enabled': False}) + # Delete domain + self.resource_api.delete_domain(domain_id) + # verify DomainNotFound raised + self.assertRaises(exception.DomainNotFound, + self.resource_api.get_domain, + domain_id) + + @unit.skip_if_cache_disabled('resource') + @unit.skip_if_no_multiple_domains_support + def test_project_rename_invalidates_get_project_by_name_cache(self): + domain = unit.new_domain_ref() + project = unit.new_project_ref(domain_id=domain['id']) + project_id = project['id'] + project_name = project['name'] + self.resource_api.create_domain(domain['id'], domain) + # Create a project + self.resource_api.create_project(project_id, project) + self.resource_api.get_project_by_name(project_name, domain['id']) + project['name'] = uuid.uuid4().hex + self.resource_api.update_project(project_id, project) + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project_by_name, + project_name, + domain['id']) + + @unit.skip_if_cache_disabled('resource') + @unit.skip_if_no_multiple_domains_support + def test_cache_layer_project_crud(self): + domain = unit.new_domain_ref() + project = unit.new_project_ref(domain_id=domain['id']) + project_id = project['id'] + self.resource_api.create_domain(domain['id'], domain) + # Create a project + self.resource_api.create_project(project_id, project) + self.resource_api.get_project(project_id) + updated_project = copy.deepcopy(project) + updated_project['name'] = uuid.uuid4().hex + # Update project, bypassing resource manager + self.resource_api.driver.update_project(project_id, + updated_project) + # Verify get_project still returns the original project_ref + self.assertDictContainsSubset( + project, self.resource_api.get_project(project_id)) + # Invalidate cache + self.resource_api.get_project.invalidate(self.resource_api, + project_id) + # Verify get_project now returns the new project + self.assertDictContainsSubset( + updated_project, + self.resource_api.get_project(project_id)) + # Update project using the resource_api manager back to original + self.resource_api.update_project(project['id'], project) + # Verify get_project returns the original project_ref + self.assertDictContainsSubset( + project, self.resource_api.get_project(project_id)) + # Delete project bypassing resource + self.resource_api.driver.delete_project(project_id) + # Verify get_project still returns the project_ref + self.assertDictContainsSubset( + project, self.resource_api.get_project(project_id)) + # Invalidate cache + self.resource_api.get_project.invalidate(self.resource_api, + project_id) + # Verify ProjectNotFound now raised + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + project_id) + # recreate project + self.resource_api.create_project(project_id, project) + self.resource_api.get_project(project_id) + # delete project + self.resource_api.delete_project(project_id) + # Verify ProjectNotFound is raised + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + project_id) + + @unit.skip_if_no_multiple_domains_support + def test_get_default_domain_by_name(self): + domain_name = 'default' + + domain = unit.new_domain_ref(name=domain_name) + self.resource_api.create_domain(domain['id'], domain) + + domain_ref = self.resource_api.get_domain_by_name(domain_name) + self.assertEqual(domain, domain_ref) + + def test_get_not_default_domain_by_name(self): + domain_name = 'foo' + self.assertRaises(exception.DomainNotFound, + self.resource_api.get_domain_by_name, + domain_name) + + def test_project_update_and_project_get_return_same_response(self): + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + + self.resource_api.create_project(project['id'], project) + + updated_project = {'enabled': False} + updated_project_ref = self.resource_api.update_project( + project['id'], updated_project) + + # SQL backend adds 'extra' field + updated_project_ref.pop('extra', None) + + self.assertIs(False, updated_project_ref['enabled']) + + project_ref = self.resource_api.get_project(project['id']) + self.assertDictEqual(updated_project_ref, project_ref) + + +class ResourceDriverTests(object): + """Tests for the resource driver. + + Subclasses must set self.driver to the driver instance. + + """ + + def test_create_project(self): + project_id = uuid.uuid4().hex + project = { + 'name': uuid.uuid4().hex, + 'id': project_id, + 'domain_id': uuid.uuid4().hex, + } + self.driver.create_project(project_id, project) + + def test_create_project_all_defined_properties(self): + project_id = uuid.uuid4().hex + project = { + 'name': uuid.uuid4().hex, + 'id': project_id, + 'domain_id': uuid.uuid4().hex, + 'description': uuid.uuid4().hex, + 'enabled': True, + 'parent_id': uuid.uuid4().hex, + 'is_domain': True, + } + self.driver.create_project(project_id, project) + + def test_create_project_null_domain(self): + project_id = uuid.uuid4().hex + project = { + 'name': uuid.uuid4().hex, + 'id': project_id, + 'domain_id': None, + } + self.driver.create_project(project_id, project) + + def test_create_project_same_name_same_domain_conflict(self): + name = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + + project_id = uuid.uuid4().hex + project = { + 'name': name, + 'id': project_id, + 'domain_id': domain_id, + } + self.driver.create_project(project_id, project) + + project_id = uuid.uuid4().hex + project = { + 'name': name, + 'id': project_id, + 'domain_id': domain_id, + } + self.assertRaises(exception.Conflict, self.driver.create_project, + project_id, project) + + def test_create_project_same_id_conflict(self): + project_id = uuid.uuid4().hex + + project = { + 'name': uuid.uuid4().hex, + 'id': project_id, + 'domain_id': uuid.uuid4().hex, + } + self.driver.create_project(project_id, project) + + project = { + 'name': uuid.uuid4().hex, + 'id': project_id, + 'domain_id': uuid.uuid4().hex, + } + self.assertRaises(exception.Conflict, self.driver.create_project, + project_id, project) diff --git a/keystone-moon/keystone/tests/unit/resource/test_controllers.py b/keystone-moon/keystone/tests/unit/resource/test_controllers.py new file mode 100644 index 00000000..b8f247c8 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/resource/test_controllers.py @@ -0,0 +1,57 @@ +# Copyright 2016 IBM Corp. +# +# 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 oslo_config import cfg + +from keystone import exception +from keystone.resource import controllers +from keystone.tests import unit +from keystone.tests.unit.ksfixtures import database + + +CONF = cfg.CONF + +_ADMIN_CONTEXT = {'is_admin': True, 'query_string': {}} + + +class TenantTestCaseNoDefaultDomain(unit.TestCase): + + def setUp(self): + super(TenantTestCaseNoDefaultDomain, self).setUp() + self.useFixture(database.Database()) + self.load_backends() + self.tenant_controller = controllers.Tenant() + + def test_setup(self): + # Other tests in this class assume there's no default domain, so make + # sure the setUp worked as expected. + self.assertRaises( + exception.DomainNotFound, + self.resource_api.get_domain, CONF.identity.default_domain_id) + + def test_get_all_projects(self): + # When get_all_projects is done and there's no default domain, the + # result is an empty list. + res = self.tenant_controller.get_all_projects(_ADMIN_CONTEXT) + self.assertEqual([], res['tenants']) + + def test_create_project(self): + # When a project is created using the v2 controller and there's no + # default domain, it doesn't fail with can't find domain (a default + # domain is created) + tenant = {'name': uuid.uuid4().hex} + self.tenant_controller.create_project(_ADMIN_CONTEXT, tenant) + # If the above doesn't fail then this is successful. diff --git a/keystone-moon/keystone/tests/unit/resource/test_core.py b/keystone-moon/keystone/tests/unit/resource/test_core.py new file mode 100644 index 00000000..2eb87e4c --- /dev/null +++ b/keystone-moon/keystone/tests/unit/resource/test_core.py @@ -0,0 +1,692 @@ +# 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 copy +import uuid + +import mock +from testtools import matchers + +from oslo_config import cfg +from oslotest import mockpatch + +from keystone import exception +from keystone.tests import unit +from keystone.tests.unit.ksfixtures import database + + +CONF = cfg.CONF + + +class TestResourceManagerNoFixtures(unit.SQLDriverOverrides, unit.TestCase): + + def setUp(self): + super(TestResourceManagerNoFixtures, self).setUp() + self.useFixture(database.Database(self.sql_driver_version_overrides)) + self.load_backends() + + def test_ensure_default_domain_exists(self): + # When there's no default domain, ensure_default_domain_exists creates + # it. + + # First make sure there's no default domain. + self.assertRaises( + exception.DomainNotFound, + self.resource_api.get_domain, CONF.identity.default_domain_id) + + self.resource_api.ensure_default_domain_exists() + default_domain = self.resource_api.get_domain( + CONF.identity.default_domain_id) + + expected_domain = { + 'id': CONF.identity.default_domain_id, + 'name': 'Default', + 'enabled': True, + 'description': 'Domain created automatically to support V2.0 ' + 'operations.', + } + self.assertEqual(expected_domain, default_domain) + + def test_ensure_default_domain_exists_already_exists(self): + # When there's already a default domain, ensure_default_domain_exists + # doesn't do anything. + + name = uuid.uuid4().hex + description = uuid.uuid4().hex + domain_attrs = { + 'id': CONF.identity.default_domain_id, + 'name': name, + 'description': description, + } + self.resource_api.create_domain(CONF.identity.default_domain_id, + domain_attrs) + + self.resource_api.ensure_default_domain_exists() + + default_domain = self.resource_api.get_domain( + CONF.identity.default_domain_id) + + expected_domain = { + 'id': CONF.identity.default_domain_id, + 'name': name, + 'enabled': True, + 'description': description, + } + + self.assertEqual(expected_domain, default_domain) + + def test_ensure_default_domain_exists_fails(self): + # When there's an unexpected exception creating domain it's passed on. + + self.useFixture(mockpatch.PatchObject( + self.resource_api, 'create_domain', + side_effect=exception.UnexpectedError)) + + self.assertRaises(exception.UnexpectedError, + self.resource_api.ensure_default_domain_exists) + + def test_update_project_name_conflict(self): + name = uuid.uuid4().hex + description = uuid.uuid4().hex + domain_attrs = { + 'id': CONF.identity.default_domain_id, + 'name': name, + 'description': description, + } + domain = self.resource_api.create_domain( + CONF.identity.default_domain_id, domain_attrs) + project1 = unit.new_project_ref(domain_id=domain['id'], + name=uuid.uuid4().hex) + self.resource_api.create_project(project1['id'], project1) + project2 = unit.new_project_ref(domain_id=domain['id'], + name=uuid.uuid4().hex) + project = self.resource_api.create_project(project2['id'], project2) + + self.assertRaises(exception.Conflict, + self.resource_api.update_project, + project['id'], {'name': project1['name']}) + + +class DomainConfigDriverTests(object): + + def _domain_config_crud(self, sensitive): + domain = uuid.uuid4().hex + group = uuid.uuid4().hex + option = uuid.uuid4().hex + value = uuid.uuid4().hex + self.driver.create_config_option( + domain, group, option, value, sensitive) + res = self.driver.get_config_option( + domain, group, option, sensitive) + config = {'group': group, 'option': option, 'value': value} + self.assertEqual(config, res) + + value = uuid.uuid4().hex + self.driver.update_config_option( + domain, group, option, value, sensitive) + res = self.driver.get_config_option( + domain, group, option, sensitive) + config = {'group': group, 'option': option, 'value': value} + self.assertEqual(config, res) + + self.driver.delete_config_options( + domain, group, option, sensitive) + self.assertRaises(exception.DomainConfigNotFound, + self.driver.get_config_option, + domain, group, option, sensitive) + # ...and silent if we try to delete it again + self.driver.delete_config_options( + domain, group, option, sensitive) + + def test_whitelisted_domain_config_crud(self): + self._domain_config_crud(sensitive=False) + + def test_sensitive_domain_config_crud(self): + self._domain_config_crud(sensitive=True) + + def _list_domain_config(self, sensitive): + """Test listing by combination of domain, group & option.""" + config1 = {'group': uuid.uuid4().hex, 'option': uuid.uuid4().hex, + 'value': uuid.uuid4().hex} + # Put config2 in the same group as config1 + config2 = {'group': config1['group'], 'option': uuid.uuid4().hex, + 'value': uuid.uuid4().hex} + config3 = {'group': uuid.uuid4().hex, 'option': uuid.uuid4().hex, + 'value': 100} + domain = uuid.uuid4().hex + + for config in [config1, config2, config3]: + self.driver.create_config_option( + domain, config['group'], config['option'], + config['value'], sensitive) + + # Try listing all items from a domain + res = self.driver.list_config_options( + domain, sensitive=sensitive) + self.assertThat(res, matchers.HasLength(3)) + for res_entry in res: + self.assertIn(res_entry, [config1, config2, config3]) + + # Try listing by domain and group + res = self.driver.list_config_options( + domain, group=config1['group'], sensitive=sensitive) + self.assertThat(res, matchers.HasLength(2)) + for res_entry in res: + self.assertIn(res_entry, [config1, config2]) + + # Try listing by domain, group and option + res = self.driver.list_config_options( + domain, group=config2['group'], + option=config2['option'], sensitive=sensitive) + self.assertThat(res, matchers.HasLength(1)) + self.assertEqual(config2, res[0]) + + def test_list_whitelisted_domain_config_crud(self): + self._list_domain_config(False) + + def test_list_sensitive_domain_config_crud(self): + self._list_domain_config(True) + + def _delete_domain_configs(self, sensitive): + """Test deleting by combination of domain, group & option.""" + config1 = {'group': uuid.uuid4().hex, 'option': uuid.uuid4().hex, + 'value': uuid.uuid4().hex} + # Put config2 and config3 in the same group as config1 + config2 = {'group': config1['group'], 'option': uuid.uuid4().hex, + 'value': uuid.uuid4().hex} + config3 = {'group': config1['group'], 'option': uuid.uuid4().hex, + 'value': uuid.uuid4().hex} + config4 = {'group': uuid.uuid4().hex, 'option': uuid.uuid4().hex, + 'value': uuid.uuid4().hex} + domain = uuid.uuid4().hex + + for config in [config1, config2, config3, config4]: + self.driver.create_config_option( + domain, config['group'], config['option'], + config['value'], sensitive) + + # Try deleting by domain, group and option + res = self.driver.delete_config_options( + domain, group=config2['group'], + option=config2['option'], sensitive=sensitive) + res = self.driver.list_config_options( + domain, sensitive=sensitive) + self.assertThat(res, matchers.HasLength(3)) + for res_entry in res: + self.assertIn(res_entry, [config1, config3, config4]) + + # Try deleting by domain and group + res = self.driver.delete_config_options( + domain, group=config4['group'], sensitive=sensitive) + res = self.driver.list_config_options( + domain, sensitive=sensitive) + self.assertThat(res, matchers.HasLength(2)) + for res_entry in res: + self.assertIn(res_entry, [config1, config3]) + + # Try deleting all items from a domain + res = self.driver.delete_config_options( + domain, sensitive=sensitive) + res = self.driver.list_config_options( + domain, sensitive=sensitive) + self.assertThat(res, matchers.HasLength(0)) + + def test_delete_whitelisted_domain_configs(self): + self._delete_domain_configs(False) + + def test_delete_sensitive_domain_configs(self): + self._delete_domain_configs(True) + + def _create_domain_config_twice(self, sensitive): + """Test conflict error thrown if create the same option twice.""" + config = {'group': uuid.uuid4().hex, 'option': uuid.uuid4().hex, + 'value': uuid.uuid4().hex} + domain = uuid.uuid4().hex + + self.driver.create_config_option( + domain, config['group'], config['option'], + config['value'], sensitive=sensitive) + self.assertRaises(exception.Conflict, + self.driver.create_config_option, + domain, config['group'], config['option'], + config['value'], sensitive=sensitive) + + def test_create_whitelisted_domain_config_twice(self): + self._create_domain_config_twice(False) + + def test_create_sensitive_domain_config_twice(self): + self._create_domain_config_twice(True) + + +class DomainConfigTests(object): + + def setUp(self): + self.domain = unit.new_domain_ref() + self.resource_api.create_domain(self.domain['id'], self.domain) + self.addCleanup(self.clean_up_domain) + + def clean_up_domain(self): + # NOTE(henry-nash): Deleting the domain will also delete any domain + # configs for this domain. + self.domain['enabled'] = False + self.resource_api.update_domain(self.domain['id'], self.domain) + self.resource_api.delete_domain(self.domain['id']) + del self.domain + + def test_create_domain_config_including_sensitive_option(self): + config = {'ldap': {'url': uuid.uuid4().hex, + 'user_tree_dn': uuid.uuid4().hex, + 'password': uuid.uuid4().hex}} + self.domain_config_api.create_config(self.domain['id'], config) + + # password is sensitive, so check that the whitelisted portion and + # the sensitive piece have been stored in the appropriate locations. + res = self.domain_config_api.get_config(self.domain['id']) + config_whitelisted = copy.deepcopy(config) + config_whitelisted['ldap'].pop('password') + self.assertEqual(config_whitelisted, res) + res = self.domain_config_api.driver.get_config_option( + self.domain['id'], 'ldap', 'password', sensitive=True) + self.assertEqual(config['ldap']['password'], res['value']) + + # Finally, use the non-public API to get back the whole config + res = self.domain_config_api.get_config_with_sensitive_info( + self.domain['id']) + self.assertEqual(config, res) + + def test_get_partial_domain_config(self): + config = {'ldap': {'url': uuid.uuid4().hex, + 'user_tree_dn': uuid.uuid4().hex, + 'password': uuid.uuid4().hex}, + 'identity': {'driver': uuid.uuid4().hex}} + self.domain_config_api.create_config(self.domain['id'], config) + + res = self.domain_config_api.get_config(self.domain['id'], + group='identity') + config_partial = copy.deepcopy(config) + config_partial.pop('ldap') + self.assertEqual(config_partial, res) + res = self.domain_config_api.get_config( + self.domain['id'], group='ldap', option='user_tree_dn') + self.assertEqual({'user_tree_dn': config['ldap']['user_tree_dn']}, res) + # ...but we should fail to get a sensitive option + self.assertRaises(exception.DomainConfigNotFound, + self.domain_config_api.get_config, self.domain['id'], + group='ldap', option='password') + + def test_delete_partial_domain_config(self): + config = {'ldap': {'url': uuid.uuid4().hex, + 'user_tree_dn': uuid.uuid4().hex, + 'password': uuid.uuid4().hex}, + 'identity': {'driver': uuid.uuid4().hex}} + self.domain_config_api.create_config(self.domain['id'], config) + + self.domain_config_api.delete_config( + self.domain['id'], group='identity') + config_partial = copy.deepcopy(config) + config_partial.pop('identity') + config_partial['ldap'].pop('password') + res = self.domain_config_api.get_config(self.domain['id']) + self.assertEqual(config_partial, res) + + self.domain_config_api.delete_config( + self.domain['id'], group='ldap', option='url') + config_partial = copy.deepcopy(config_partial) + config_partial['ldap'].pop('url') + res = self.domain_config_api.get_config(self.domain['id']) + self.assertEqual(config_partial, res) + + def test_get_options_not_in_domain_config(self): + self.assertRaises(exception.DomainConfigNotFound, + self.domain_config_api.get_config, self.domain['id']) + config = {'ldap': {'url': uuid.uuid4().hex}} + + self.domain_config_api.create_config(self.domain['id'], config) + + self.assertRaises(exception.DomainConfigNotFound, + self.domain_config_api.get_config, self.domain['id'], + group='identity') + self.assertRaises(exception.DomainConfigNotFound, + self.domain_config_api.get_config, self.domain['id'], + group='ldap', option='user_tree_dn') + + def test_get_sensitive_config(self): + config = {'ldap': {'url': uuid.uuid4().hex, + 'user_tree_dn': uuid.uuid4().hex, + 'password': uuid.uuid4().hex}, + 'identity': {'driver': uuid.uuid4().hex}} + res = self.domain_config_api.get_config_with_sensitive_info( + self.domain['id']) + self.assertEqual({}, res) + self.domain_config_api.create_config(self.domain['id'], config) + res = self.domain_config_api.get_config_with_sensitive_info( + self.domain['id']) + self.assertEqual(config, res) + + def test_update_partial_domain_config(self): + config = {'ldap': {'url': uuid.uuid4().hex, + 'user_tree_dn': uuid.uuid4().hex, + 'password': uuid.uuid4().hex}, + 'identity': {'driver': uuid.uuid4().hex}} + self.domain_config_api.create_config(self.domain['id'], config) + + # Try updating a group + new_config = {'ldap': {'url': uuid.uuid4().hex, + 'user_filter': uuid.uuid4().hex}} + res = self.domain_config_api.update_config( + self.domain['id'], new_config, group='ldap') + expected_config = copy.deepcopy(config) + expected_config['ldap']['url'] = new_config['ldap']['url'] + expected_config['ldap']['user_filter'] = ( + new_config['ldap']['user_filter']) + expected_full_config = copy.deepcopy(expected_config) + expected_config['ldap'].pop('password') + res = self.domain_config_api.get_config(self.domain['id']) + self.assertEqual(expected_config, res) + # The sensitive option should still exist + res = self.domain_config_api.get_config_with_sensitive_info( + self.domain['id']) + self.assertEqual(expected_full_config, res) + + # Try updating a single whitelisted option + self.domain_config_api.delete_config(self.domain['id']) + self.domain_config_api.create_config(self.domain['id'], config) + new_config = {'url': uuid.uuid4().hex} + res = self.domain_config_api.update_config( + self.domain['id'], new_config, group='ldap', option='url') + + # Make sure whitelisted and full config is updated + expected_whitelisted_config = copy.deepcopy(config) + expected_whitelisted_config['ldap']['url'] = new_config['url'] + expected_full_config = copy.deepcopy(expected_whitelisted_config) + expected_whitelisted_config['ldap'].pop('password') + self.assertEqual(expected_whitelisted_config, res) + res = self.domain_config_api.get_config(self.domain['id']) + self.assertEqual(expected_whitelisted_config, res) + res = self.domain_config_api.get_config_with_sensitive_info( + self.domain['id']) + self.assertEqual(expected_full_config, res) + + # Try updating a single sensitive option + self.domain_config_api.delete_config(self.domain['id']) + self.domain_config_api.create_config(self.domain['id'], config) + new_config = {'password': uuid.uuid4().hex} + res = self.domain_config_api.update_config( + self.domain['id'], new_config, group='ldap', option='password') + # The whitelisted config should not have changed... + expected_whitelisted_config = copy.deepcopy(config) + expected_full_config = copy.deepcopy(config) + expected_whitelisted_config['ldap'].pop('password') + self.assertEqual(expected_whitelisted_config, res) + res = self.domain_config_api.get_config(self.domain['id']) + self.assertEqual(expected_whitelisted_config, res) + expected_full_config['ldap']['password'] = new_config['password'] + res = self.domain_config_api.get_config_with_sensitive_info( + self.domain['id']) + # ...but the sensitive piece should have. + self.assertEqual(expected_full_config, res) + + def test_update_invalid_partial_domain_config(self): + config = {'ldap': {'url': uuid.uuid4().hex, + 'user_tree_dn': uuid.uuid4().hex, + 'password': uuid.uuid4().hex}, + 'identity': {'driver': uuid.uuid4().hex}} + # An extra group, when specifying one group should fail + self.assertRaises(exception.InvalidDomainConfig, + self.domain_config_api.update_config, + self.domain['id'], config, group='ldap') + # An extra option, when specifying one option should fail + self.assertRaises(exception.InvalidDomainConfig, + self.domain_config_api.update_config, + self.domain['id'], config['ldap'], + group='ldap', option='url') + + # Now try the right number of groups/options, but just not + # ones that are in the config provided + config = {'ldap': {'user_tree_dn': uuid.uuid4().hex}} + self.assertRaises(exception.InvalidDomainConfig, + self.domain_config_api.update_config, + self.domain['id'], config, group='identity') + self.assertRaises(exception.InvalidDomainConfig, + self.domain_config_api.update_config, + self.domain['id'], config['ldap'], group='ldap', + option='url') + + # Now some valid groups/options, but just not ones that are in the + # existing config + config = {'ldap': {'user_tree_dn': uuid.uuid4().hex}} + self.domain_config_api.create_config(self.domain['id'], config) + config_wrong_group = {'identity': {'driver': uuid.uuid4().hex}} + self.assertRaises(exception.DomainConfigNotFound, + self.domain_config_api.update_config, + self.domain['id'], config_wrong_group, + group='identity') + config_wrong_option = {'url': uuid.uuid4().hex} + self.assertRaises(exception.DomainConfigNotFound, + self.domain_config_api.update_config, + self.domain['id'], config_wrong_option, + group='ldap', option='url') + + # And finally just some bad groups/options + bad_group = uuid.uuid4().hex + config = {bad_group: {'user': uuid.uuid4().hex}} + self.assertRaises(exception.InvalidDomainConfig, + self.domain_config_api.update_config, + self.domain['id'], config, group=bad_group, + option='user') + bad_option = uuid.uuid4().hex + config = {'ldap': {bad_option: uuid.uuid4().hex}} + self.assertRaises(exception.InvalidDomainConfig, + self.domain_config_api.update_config, + self.domain['id'], config, group='ldap', + option=bad_option) + + def test_create_invalid_domain_config(self): + self.assertRaises(exception.InvalidDomainConfig, + self.domain_config_api.create_config, + self.domain['id'], {}) + config = {uuid.uuid4().hex: uuid.uuid4().hex} + self.assertRaises(exception.InvalidDomainConfig, + self.domain_config_api.create_config, + self.domain['id'], config) + config = {uuid.uuid4().hex: {uuid.uuid4().hex: uuid.uuid4().hex}} + self.assertRaises(exception.InvalidDomainConfig, + self.domain_config_api.create_config, + self.domain['id'], config) + config = {'ldap': {uuid.uuid4().hex: uuid.uuid4().hex}} + self.assertRaises(exception.InvalidDomainConfig, + self.domain_config_api.create_config, + self.domain['id'], config) + # Try an option that IS in the standard conf, but neither whitelisted + # or marked as sensitive + config = {'identity': {'user_tree_dn': uuid.uuid4().hex}} + self.assertRaises(exception.InvalidDomainConfig, + self.domain_config_api.create_config, + self.domain['id'], config) + + def test_delete_invalid_partial_domain_config(self): + config = {'ldap': {'url': uuid.uuid4().hex}} + self.domain_config_api.create_config(self.domain['id'], config) + # Try deleting a group not in the config + self.assertRaises(exception.DomainConfigNotFound, + self.domain_config_api.delete_config, + self.domain['id'], group='identity') + # Try deleting an option not in the config + self.assertRaises(exception.DomainConfigNotFound, + self.domain_config_api.delete_config, + self.domain['id'], + group='ldap', option='user_tree_dn') + + def test_sensitive_substitution_in_domain_config(self): + # Create a config that contains a whitelisted option that requires + # substitution of a sensitive option. + config = {'ldap': {'url': 'my_url/%(password)s', + 'user_tree_dn': uuid.uuid4().hex, + 'password': uuid.uuid4().hex}, + 'identity': {'driver': uuid.uuid4().hex}} + self.domain_config_api.create_config(self.domain['id'], config) + + # Read back the config with the internal method and ensure that the + # substitution has taken place. + res = self.domain_config_api.get_config_with_sensitive_info( + self.domain['id']) + expected_url = ( + config['ldap']['url'] % {'password': config['ldap']['password']}) + self.assertEqual(expected_url, res['ldap']['url']) + + def test_invalid_sensitive_substitution_in_domain_config(self): + """Check that invalid substitutions raise warnings.""" + mock_log = mock.Mock() + + invalid_option_config = { + 'ldap': {'user_tree_dn': uuid.uuid4().hex, + 'password': uuid.uuid4().hex}, + 'identity': {'driver': uuid.uuid4().hex}} + + for invalid_option in ['my_url/%(passssword)s', + 'my_url/%(password', + 'my_url/%(password)', + 'my_url/%(password)d']: + invalid_option_config['ldap']['url'] = invalid_option + self.domain_config_api.create_config( + self.domain['id'], invalid_option_config) + + with mock.patch('keystone.resource.core.LOG', mock_log): + res = self.domain_config_api.get_config_with_sensitive_info( + self.domain['id']) + mock_log.warning.assert_any_call(mock.ANY) + self.assertEqual( + invalid_option_config['ldap']['url'], res['ldap']['url']) + + def test_escaped_sequence_in_domain_config(self): + """Check that escaped '%(' doesn't get interpreted.""" + mock_log = mock.Mock() + + escaped_option_config = { + 'ldap': {'url': 'my_url/%%(password)s', + 'user_tree_dn': uuid.uuid4().hex, + 'password': uuid.uuid4().hex}, + 'identity': {'driver': uuid.uuid4().hex}} + + self.domain_config_api.create_config( + self.domain['id'], escaped_option_config) + + with mock.patch('keystone.resource.core.LOG', mock_log): + res = self.domain_config_api.get_config_with_sensitive_info( + self.domain['id']) + self.assertFalse(mock_log.warn.called) + # The escaping '%' should have been removed + self.assertEqual('my_url/%(password)s', res['ldap']['url']) + + @unit.skip_if_cache_disabled('domain_config') + def test_cache_layer_get_sensitive_config(self): + config = {'ldap': {'url': uuid.uuid4().hex, + 'user_tree_dn': uuid.uuid4().hex, + 'password': uuid.uuid4().hex}, + 'identity': {'driver': uuid.uuid4().hex}} + self.domain_config_api.create_config(self.domain['id'], config) + # cache the result + res = self.domain_config_api.get_config_with_sensitive_info( + self.domain['id']) + self.assertEqual(config, res) + + # delete, bypassing domain config manager api + self.domain_config_api.delete_config_options(self.domain['id']) + self.domain_config_api.delete_config_options(self.domain['id'], + sensitive=True) + + self.assertDictEqual( + res, self.domain_config_api.get_config_with_sensitive_info( + self.domain['id'])) + self.domain_config_api.get_config_with_sensitive_info.invalidate( + self.domain_config_api, self.domain['id']) + self.assertDictEqual( + {}, + self.domain_config_api.get_config_with_sensitive_info( + self.domain['id'])) + + def test_delete_domain_deletes_configs(self): + """Test domain deletion clears the domain configs.""" + domain = unit.new_domain_ref() + self.resource_api.create_domain(domain['id'], domain) + config = {'ldap': {'url': uuid.uuid4().hex, + 'user_tree_dn': uuid.uuid4().hex, + 'password': uuid.uuid4().hex}} + self.domain_config_api.create_config(domain['id'], config) + + # Now delete the domain + domain['enabled'] = False + self.resource_api.update_domain(domain['id'], domain) + self.resource_api.delete_domain(domain['id']) + + # Check domain configs have also been deleted + self.assertRaises( + exception.DomainConfigNotFound, + self.domain_config_api.get_config, + domain['id']) + + # The get_config_with_sensitive_info does not throw an exception if + # the config is empty, it just returns an empty dict + self.assertDictEqual( + {}, + self.domain_config_api.get_config_with_sensitive_info( + domain['id'])) + + def test_config_registration(self): + type = uuid.uuid4().hex + self.domain_config_api.obtain_registration( + self.domain['id'], type) + self.domain_config_api.release_registration( + self.domain['id'], type=type) + + # Make sure that once someone has it, nobody else can get it. + # This includes the domain who already has it. + self.domain_config_api.obtain_registration( + self.domain['id'], type) + self.assertFalse( + self.domain_config_api.obtain_registration( + self.domain['id'], type)) + + # Make sure we can read who does have it + self.assertEqual( + self.domain['id'], + self.domain_config_api.read_registration(type)) + + # Make sure releasing it is silent if the domain specified doesn't + # have the registration + domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.resource_api.create_domain(domain2['id'], domain2) + self.domain_config_api.release_registration( + domain2['id'], type=type) + + # If nobody has the type registered, then trying to read it should + # raise ConfigRegistrationNotFound + self.domain_config_api.release_registration( + self.domain['id'], type=type) + self.assertRaises(exception.ConfigRegistrationNotFound, + self.domain_config_api.read_registration, + type) + + # Finally check multiple registrations are cleared if you free the + # registration without specifying the type + type2 = uuid.uuid4().hex + self.domain_config_api.obtain_registration( + self.domain['id'], type) + self.domain_config_api.obtain_registration( + self.domain['id'], type2) + self.domain_config_api.release_registration(self.domain['id']) + self.assertRaises(exception.ConfigRegistrationNotFound, + self.domain_config_api.read_registration, + type) + self.assertRaises(exception.ConfigRegistrationNotFound, + self.domain_config_api.read_registration, + type2) diff --git a/keystone-moon/keystone/tests/unit/rest.py b/keystone-moon/keystone/tests/unit/rest.py index 35b47e2b..512c301d 100644 --- a/keystone-moon/keystone/tests/unit/rest.py +++ b/keystone-moon/keystone/tests/unit/rest.py @@ -61,7 +61,7 @@ class RestfulTestCase(unit.TestCase): # Will need to reset the plug-ins self.addCleanup(setattr, auth_controllers, 'AUTH_METHODS', {}) - self.useFixture(database.Database()) + self.useFixture(database.Database(self.sql_driver_version_overrides)) self.load_backends() self.load_fixtures(default_fixtures) @@ -114,11 +114,10 @@ class RestfulTestCase(unit.TestCase): example:: - self.assertResponseStatus(response, 204) + self.assertResponseStatus(response, http_client.NO_CONTENT) """ self.assertEqual( - response.status_code, - expected_status, + expected_status, response.status_code, 'Status code %s is not %s, as expected\n\n%s' % (response.status_code, expected_status, response.body)) @@ -133,9 +132,9 @@ class RestfulTestCase(unit.TestCase): Subclasses can override this function based on the expected response. """ - self.assertEqual(response.status_code, expected_status) + self.assertEqual(expected_status, response.status_code) error = response.result['error'] - self.assertEqual(error['code'], response.status_code) + self.assertEqual(response.status_code, error['code']) self.assertIsNotNone(error.get('title')) def _to_content_type(self, body, headers, content_type=None): @@ -146,7 +145,11 @@ class RestfulTestCase(unit.TestCase): headers['Accept'] = 'application/json' if body: headers['Content-Type'] = 'application/json' - return jsonutils.dumps(body) + # NOTE(davechen):dump the body to bytes since WSGI requires + # the body of the response to be `Bytestrings`. + # see pep-3333: + # https://www.python.org/dev/peps/pep-3333/#a-note-on-string-types + return jsonutils.dump_as_bytes(body) def _from_content_type(self, response, content_type=None): """Attempt to decode JSON and XML automatically, if detected.""" @@ -213,6 +216,17 @@ class RestfulTestCase(unit.TestCase): r = self.public_request(method='POST', path='/v2.0/tokens', body=body) return self._get_token_id(r) + def get_admin_token(self): + return self._get_token({ + 'auth': { + 'passwordCredentials': { + 'username': self.user_reqadmin['name'], + 'password': self.user_reqadmin['password'] + }, + 'tenantId': default_fixtures.SERVICE_TENANT_ID + } + }) + def get_unscoped_token(self): """Convenience method so that we can test authenticated requests.""" return self._get_token({ diff --git a/keystone-moon/keystone/tests/unit/schema/__init__.py b/keystone-moon/keystone/tests/unit/schema/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/unit/schema/v2.py b/keystone-moon/keystone/tests/unit/schema/v2.py new file mode 100644 index 00000000..ed260a00 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/schema/v2.py @@ -0,0 +1,161 @@ +# 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 copy + +from keystone.common import validation +from keystone.common.validation import parameter_types +from keystone.common.validation import validators + + +_project_properties = { + 'id': parameter_types.id_string, + 'name': parameter_types.name, + 'enabled': parameter_types.boolean, + 'description': validation.nullable(parameter_types.description), +} + +_token_properties = { + 'audit_ids': { + 'type': 'array', + 'items': { + 'type': 'string', + }, + 'minItems': 1, + 'maxItems': 2, + }, + 'id': {'type': 'string'}, + 'expires': {'type': 'string'}, + 'issued_at': {'type': 'string'}, + 'tenant': { + 'type': 'object', + 'properties': _project_properties, + 'required': ['id', 'name', 'enabled'], + 'additionalProperties': False, + }, +} + +_role_properties = { + 'name': parameter_types.name, +} + +_user_properties = { + 'id': parameter_types.id_string, + 'name': parameter_types.name, + 'username': parameter_types.name, + 'roles': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': _role_properties, + 'required': ['name'], + 'additionalProperties': False, + }, + }, + 'roles_links': { + 'type': 'array', + 'maxItems': 0, + }, +} + +_metadata_properties = { + 'is_admin': {'type': 'integer'}, + 'roles': { + 'type': 'array', + 'items': {'type': 'string'}, + }, +} + +_endpoint_properties = { + 'id': {'type': 'string'}, + 'adminURL': parameter_types.url, + 'internalURL': parameter_types.url, + 'publicURL': parameter_types.url, + 'region': {'type': 'string'}, +} + +_service_properties = { + 'type': {'type': 'string'}, + 'name': parameter_types.name, + 'endpoints_links': { + 'type': 'array', + 'maxItems': 0, + }, + 'endpoints': { + 'type': 'array', + 'minItems': 1, + 'items': { + 'type': 'object', + 'properties': _endpoint_properties, + 'required': ['id', 'publicURL'], + 'additionalProperties': False, + }, + }, +} + +_base_access_properties = { + 'metadata': { + 'type': 'object', + 'properties': _metadata_properties, + 'required': ['is_admin', 'roles'], + 'additionalProperties': False, + }, + 'serviceCatalog': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': _service_properties, + 'required': ['name', 'type', 'endpoints_links', 'endpoints'], + 'additionalProperties': False, + }, + }, + 'token': { + 'type': 'object', + 'properties': _token_properties, + 'required': ['audit_ids', 'id', 'expires', 'issued_at'], + 'additionalProperties': False, + }, + 'user': { + 'type': 'object', + 'properties': _user_properties, + 'required': ['id', 'name', 'username', 'roles', 'roles_links'], + 'additionalProperties': False, + }, +} + +_unscoped_access_properties = copy.deepcopy(_base_access_properties) +unscoped_metadata = _unscoped_access_properties['metadata'] +unscoped_metadata['properties']['roles']['maxItems'] = 0 +_unscoped_access_properties['user']['properties']['roles']['maxItems'] = 0 +_unscoped_access_properties['serviceCatalog']['maxItems'] = 0 + +_scoped_access_properties = copy.deepcopy(_base_access_properties) +_scoped_access_properties['metadata']['properties']['roles']['minItems'] = 1 +_scoped_access_properties['serviceCatalog']['minItems'] = 1 +_scoped_access_properties['user']['properties']['roles']['minItems'] = 1 + +base_token_schema = { + 'type': 'object', + 'required': ['metadata', 'user', 'serviceCatalog', 'token'], + 'additionalProperties': False, +} + +unscoped_token_schema = copy.deepcopy(base_token_schema) +unscoped_token_schema['properties'] = _unscoped_access_properties + +scoped_token_schema = copy.deepcopy(base_token_schema) +scoped_token_schema['properties'] = _scoped_access_properties + +# Validator objects +unscoped_validator = validators.SchemaValidator(unscoped_token_schema) +scoped_validator = validators.SchemaValidator(scoped_token_schema) diff --git a/keystone-moon/keystone/tests/unit/test_associate_project_endpoint_extension.py b/keystone-moon/keystone/tests/unit/test_associate_project_endpoint_extension.py index 24fc82dd..79065863 100644 --- a/keystone-moon/keystone/tests/unit/test_associate_project_endpoint_extension.py +++ b/keystone-moon/keystone/tests/unit/test_associate_project_endpoint_extension.py @@ -15,24 +15,25 @@ import copy import uuid +import mock +from oslo_log import versionutils from six.moves import http_client from testtools import matchers +from keystone.contrib.endpoint_filter import routers +from keystone.tests import unit from keystone.tests.unit import test_v3 -class TestExtensionCase(test_v3.RestfulTestCase): - - EXTENSION_NAME = 'endpoint_filter' - EXTENSION_TO_ADD = 'endpoint_filter_extension' +class EndpointFilterTestCase(test_v3.RestfulTestCase): def config_overrides(self): - super(TestExtensionCase, self).config_overrides() + super(EndpointFilterTestCase, self).config_overrides() self.config_fixture.config( group='catalog', driver='endpoint_filter.sql') def setUp(self): - super(TestExtensionCase, self).setUp() + super(EndpointFilterTestCase, self).setUp() self.default_request_url = ( '/OS-EP-FILTER/projects/%(project_id)s' '/endpoints/%(endpoint_id)s' % { @@ -40,7 +41,17 @@ class TestExtensionCase(test_v3.RestfulTestCase): 'endpoint_id': self.endpoint_id}) -class EndpointFilterCRUDTestCase(TestExtensionCase): +class EndpointFilterDeprecateTestCase(test_v3.RestfulTestCase): + + @mock.patch.object(versionutils, 'report_deprecated_feature') + def test_exception_happens(self, mock_deprecator): + routers.EndpointFilterExtension(mock.ANY) + mock_deprecator.assert_called_once_with(mock.ANY, mock.ANY) + args, _kwargs = mock_deprecator.call_args + self.assertIn("Remove endpoint_filter_extension from", args[1]) + + +class EndpointFilterCRUDTestCase(EndpointFilterTestCase): def test_create_endpoint_project_association(self): """PUT /OS-EP-FILTER/projects/{project_id}/endpoints/{endpoint_id} @@ -48,8 +59,7 @@ class EndpointFilterCRUDTestCase(TestExtensionCase): Valid endpoint and project id test case. """ - self.put(self.default_request_url, - expected_status=204) + self.put(self.default_request_url) def test_create_endpoint_project_association_with_invalid_project(self): """PUT OS-EP-FILTER/projects/{project_id}/endpoints/{endpoint_id} @@ -82,8 +92,7 @@ class EndpointFilterCRUDTestCase(TestExtensionCase): """ self.put(self.default_request_url, - body={'project_id': self.default_domain_project_id}, - expected_status=204) + body={'project_id': self.default_domain_project_id}) def test_check_endpoint_project_association(self): """HEAD /OS-EP-FILTER/projects/{project_id}/endpoints/{endpoint_id} @@ -91,13 +100,11 @@ class EndpointFilterCRUDTestCase(TestExtensionCase): Valid project and endpoint id test case. """ - self.put(self.default_request_url, - expected_status=204) + self.put(self.default_request_url) self.head('/OS-EP-FILTER/projects/%(project_id)s' '/endpoints/%(endpoint_id)s' % { 'project_id': self.default_domain_project_id, - 'endpoint_id': self.endpoint_id}, - expected_status=204) + 'endpoint_id': self.endpoint_id}) def test_check_endpoint_project_association_with_invalid_project(self): """HEAD /OS-EP-FILTER/projects/{project_id}/endpoints/{endpoint_id} @@ -169,8 +176,7 @@ class EndpointFilterCRUDTestCase(TestExtensionCase): """ r = self.get('/OS-EP-FILTER/endpoints/%(endpoint_id)s/projects' % - {'endpoint_id': self.endpoint_id}, - expected_status=200) + {'endpoint_id': self.endpoint_id}) self.assertValidProjectListResponse(r, expected_length=0) def test_list_projects_associated_with_invalid_endpoint(self): @@ -193,8 +199,7 @@ class EndpointFilterCRUDTestCase(TestExtensionCase): self.delete('/OS-EP-FILTER/projects/%(project_id)s' '/endpoints/%(endpoint_id)s' % { 'project_id': self.default_domain_project_id, - 'endpoint_id': self.endpoint_id}, - expected_status=204) + 'endpoint_id': self.endpoint_id}) def test_remove_endpoint_project_association_with_invalid_project(self): """DELETE /OS-EP-FILTER/projects/{project_id}/endpoints/{endpoint_id} @@ -226,35 +231,167 @@ class EndpointFilterCRUDTestCase(TestExtensionCase): self.put(self.default_request_url) association_url = ('/OS-EP-FILTER/endpoints/%(endpoint_id)s/projects' % {'endpoint_id': self.endpoint_id}) - r = self.get(association_url, expected_status=200) + r = self.get(association_url) self.assertValidProjectListResponse(r, expected_length=1) self.delete('/projects/%(project_id)s' % { 'project_id': self.default_domain_project_id}) - r = self.get(association_url, expected_status=200) + r = self.get(association_url) self.assertValidProjectListResponse(r, expected_length=0) def test_endpoint_project_association_cleanup_when_endpoint_deleted(self): self.put(self.default_request_url) association_url = '/OS-EP-FILTER/projects/%(project_id)s/endpoints' % { 'project_id': self.default_domain_project_id} - r = self.get(association_url, expected_status=200) + r = self.get(association_url) self.assertValidEndpointListResponse(r, expected_length=1) self.delete('/endpoints/%(endpoint_id)s' % { 'endpoint_id': self.endpoint_id}) - r = self.get(association_url, expected_status=200) + r = self.get(association_url) self.assertValidEndpointListResponse(r, expected_length=0) + @unit.skip_if_cache_disabled('catalog') + def test_create_endpoint_project_association_invalidates_cache(self): + # NOTE(davechen): create another endpoint which will be added to + # default project, this should be done at first since + # `create_endpoint` will also invalidate cache. + endpoint_id2 = uuid.uuid4().hex + endpoint2 = unit.new_endpoint_ref(service_id=self.service_id, + region_id=self.region_id, + interface='public', + id=endpoint_id2) + self.catalog_api.create_endpoint(endpoint_id2, endpoint2.copy()) + + # create endpoint project association. + self.put(self.default_request_url) -class EndpointFilterTokenRequestTestCase(TestExtensionCase): + # should get back only one endpoint that was just created. + user_id = uuid.uuid4().hex + catalog = self.catalog_api.get_v3_catalog( + user_id, + self.default_domain_project_id) + + # there is only one endpoints associated with the default project. + self.assertEqual(1, len(catalog[0]['endpoints'])) + self.assertEqual(self.endpoint_id, catalog[0]['endpoints'][0]['id']) + + # add the second endpoint to default project, bypassing + # catalog_api API manager. + self.catalog_api.driver.add_endpoint_to_project( + endpoint_id2, + self.default_domain_project_id) + + # but, we can just get back one endpoint from the cache, since the + # catalog is pulled out from cache and its haven't been invalidated. + catalog = self.catalog_api.get_v3_catalog( + user_id, + self.default_domain_project_id) + + self.assertEqual(1, len(catalog[0]['endpoints'])) + + # remove the endpoint2 from the default project, and add it again via + # catalog_api API manager. + self.catalog_api.driver.remove_endpoint_from_project( + endpoint_id2, + self.default_domain_project_id) + + # add second endpoint to default project, this can be done by calling + # the catalog_api API manager directly but call the REST API + # instead for consistency. + self.put('/OS-EP-FILTER/projects/%(project_id)s' + '/endpoints/%(endpoint_id)s' % { + 'project_id': self.default_domain_project_id, + 'endpoint_id': endpoint_id2}) + + # should get back two endpoints since the cache has been + # invalidated when the second endpoint was added to default project. + catalog = self.catalog_api.get_v3_catalog( + user_id, + self.default_domain_project_id) + + self.assertEqual(2, len(catalog[0]['endpoints'])) + + ep_id_list = [catalog[0]['endpoints'][0]['id'], + catalog[0]['endpoints'][1]['id']] + self.assertItemsEqual([self.endpoint_id, endpoint_id2], ep_id_list) + + @unit.skip_if_cache_disabled('catalog') + def test_remove_endpoint_from_project_invalidates_cache(self): + endpoint_id2 = uuid.uuid4().hex + endpoint2 = unit.new_endpoint_ref(service_id=self.service_id, + region_id=self.region_id, + interface='public', + id=endpoint_id2) + self.catalog_api.create_endpoint(endpoint_id2, endpoint2.copy()) + # create endpoint project association. + self.put(self.default_request_url) + + # add second endpoint to default project. + self.put('/OS-EP-FILTER/projects/%(project_id)s' + '/endpoints/%(endpoint_id)s' % { + 'project_id': self.default_domain_project_id, + 'endpoint_id': endpoint_id2}) + + # should get back only one endpoint that was just created. + user_id = uuid.uuid4().hex + catalog = self.catalog_api.get_v3_catalog( + user_id, + self.default_domain_project_id) + + # there are two endpoints associated with the default project. + ep_id_list = [catalog[0]['endpoints'][0]['id'], + catalog[0]['endpoints'][1]['id']] + self.assertEqual(2, len(catalog[0]['endpoints'])) + self.assertItemsEqual([self.endpoint_id, endpoint_id2], ep_id_list) + + # remove the endpoint2 from the default project, bypassing + # catalog_api API manager. + self.catalog_api.driver.remove_endpoint_from_project( + endpoint_id2, + self.default_domain_project_id) + + # but, we can just still get back two endpoints from the cache, + # since the catalog is pulled out from cache and its haven't + # been invalidated. + catalog = self.catalog_api.get_v3_catalog( + user_id, + self.default_domain_project_id) + + self.assertEqual(2, len(catalog[0]['endpoints'])) + + # add back the endpoint2 to the default project, and remove it by + # catalog_api API manage. + self.catalog_api.driver.add_endpoint_to_project( + endpoint_id2, + self.default_domain_project_id) + + # remove the endpoint2 from the default project, this can be done + # by calling the catalog_api API manager directly but call + # the REST API instead for consistency. + self.delete('/OS-EP-FILTER/projects/%(project_id)s' + '/endpoints/%(endpoint_id)s' % { + 'project_id': self.default_domain_project_id, + 'endpoint_id': endpoint_id2}) + + # should only get back one endpoint since the cache has been + # invalidated after the endpoint project association was removed. + catalog = self.catalog_api.get_v3_catalog( + user_id, + self.default_domain_project_id) + + self.assertEqual(1, len(catalog[0]['endpoints'])) + self.assertEqual(self.endpoint_id, catalog[0]['endpoints'][0]['id']) + + +class EndpointFilterTokenRequestTestCase(EndpointFilterTestCase): def test_project_scoped_token_using_endpoint_filter(self): """Verify endpoints from project scoped token filtered.""" # create a project to work with - ref = self.new_project_ref(domain_id=self.domain_id) + ref = unit.new_project_ref(domain_id=self.domain_id) r = self.post('/projects', body={'project': ref}) project = self.assertValidProjectResponse(r, ref) @@ -276,8 +413,7 @@ class EndpointFilterTokenRequestTestCase(TestExtensionCase): self.put('/OS-EP-FILTER/projects/%(project_id)s' '/endpoints/%(endpoint_id)s' % { 'project_id': project['id'], - 'endpoint_id': self.endpoint_id}, - expected_status=204) + 'endpoint_id': self.endpoint_id}) # attempt to authenticate without requesting a project auth_data = self.build_authentication_request( @@ -289,7 +425,7 @@ class EndpointFilterTokenRequestTestCase(TestExtensionCase): require_catalog=True, endpoint_filter=True, ep_filter_assoc=1) - self.assertEqual(r.result['token']['project']['id'], project['id']) + self.assertEqual(project['id'], r.result['token']['project']['id']) def test_default_scoped_token_using_endpoint_filter(self): """Verify endpoints from default scoped token filtered.""" @@ -297,8 +433,7 @@ class EndpointFilterTokenRequestTestCase(TestExtensionCase): self.put('/OS-EP-FILTER/projects/%(project_id)s' '/endpoints/%(endpoint_id)s' % { 'project_id': self.project['id'], - 'endpoint_id': self.endpoint_id}, - expected_status=204) + 'endpoint_id': self.endpoint_id}) auth_data = self.build_authentication_request( user_id=self.user['id'], @@ -310,16 +445,24 @@ class EndpointFilterTokenRequestTestCase(TestExtensionCase): require_catalog=True, endpoint_filter=True, ep_filter_assoc=1) - self.assertEqual(r.result['token']['project']['id'], - self.project['id']) + self.assertEqual(self.project['id'], + r.result['token']['project']['id']) + + # Ensure name of the service exists + self.assertIn('name', r.result['token']['catalog'][0]) + + # region and region_id should be the same in endpoints + endpoint = r.result['token']['catalog'][0]['endpoints'][0] + self.assertIn('region', endpoint) + self.assertIn('region_id', endpoint) + self.assertEqual(endpoint['region'], endpoint['region_id']) def test_scoped_token_with_no_catalog_using_endpoint_filter(self): """Verify endpoint filter does not affect no catalog.""" self.put('/OS-EP-FILTER/projects/%(project_id)s' '/endpoints/%(endpoint_id)s' % { 'project_id': self.project['id'], - 'endpoint_id': self.endpoint_id}, - expected_status=204) + 'endpoint_id': self.endpoint_id}) auth_data = self.build_authentication_request( user_id=self.user['id'], @@ -329,8 +472,8 @@ class EndpointFilterTokenRequestTestCase(TestExtensionCase): self.assertValidProjectScopedTokenResponse( r, require_catalog=False) - self.assertEqual(r.result['token']['project']['id'], - self.project['id']) + self.assertEqual(self.project['id'], + r.result['token']['project']['id']) def test_invalid_endpoint_project_association(self): """Verify an invalid endpoint-project association is handled.""" @@ -338,28 +481,26 @@ class EndpointFilterTokenRequestTestCase(TestExtensionCase): self.put('/OS-EP-FILTER/projects/%(project_id)s' '/endpoints/%(endpoint_id)s' % { 'project_id': self.project['id'], - 'endpoint_id': self.endpoint_id}, - expected_status=204) + 'endpoint_id': self.endpoint_id}) # create a second temporary endpoint - self.endpoint_id2 = uuid.uuid4().hex - self.endpoint2 = self.new_endpoint_ref(service_id=self.service_id) - self.endpoint2['id'] = self.endpoint_id2 - self.catalog_api.create_endpoint( - self.endpoint_id2, - self.endpoint2.copy()) + endpoint_id2 = uuid.uuid4().hex + endpoint2 = unit.new_endpoint_ref(service_id=self.service_id, + region_id=self.region_id, + interface='public', + id=endpoint_id2) + self.catalog_api.create_endpoint(endpoint_id2, endpoint2.copy()) # add second endpoint to default project self.put('/OS-EP-FILTER/projects/%(project_id)s' '/endpoints/%(endpoint_id)s' % { 'project_id': self.project['id'], - 'endpoint_id': self.endpoint_id2}, - expected_status=204) + 'endpoint_id': endpoint_id2}) # remove the temporary reference # this will create inconsistency in the endpoint filter table # which is fixed during the catalog creation for token request - self.catalog_api.delete_endpoint(self.endpoint_id2) + self.catalog_api.delete_endpoint(endpoint_id2) auth_data = self.build_authentication_request( user_id=self.user['id'], @@ -371,8 +512,8 @@ class EndpointFilterTokenRequestTestCase(TestExtensionCase): require_catalog=True, endpoint_filter=True, ep_filter_assoc=1) - self.assertEqual(r.result['token']['project']['id'], - self.project['id']) + self.assertEqual(self.project['id'], + r.result['token']['project']['id']) def test_disabled_endpoint(self): """Test that a disabled endpoint is handled.""" @@ -380,8 +521,7 @@ class EndpointFilterTokenRequestTestCase(TestExtensionCase): self.put('/OS-EP-FILTER/projects/%(project_id)s' '/endpoints/%(endpoint_id)s' % { 'project_id': self.project['id'], - 'endpoint_id': self.endpoint_id}, - expected_status=204) + 'endpoint_id': self.endpoint_id}) # Add a disabled endpoint to the default project. @@ -399,8 +539,7 @@ class EndpointFilterTokenRequestTestCase(TestExtensionCase): self.put('/OS-EP-FILTER/projects/%(project_id)s' '/endpoints/%(endpoint_id)s' % { 'project_id': self.project['id'], - 'endpoint_id': disabled_endpoint_id}, - expected_status=204) + 'endpoint_id': disabled_endpoint_id}) # Authenticate to get token with catalog auth_data = self.build_authentication_request( @@ -416,7 +555,9 @@ class EndpointFilterTokenRequestTestCase(TestExtensionCase): def test_multiple_endpoint_project_associations(self): def _create_an_endpoint(): - endpoint_ref = self.new_endpoint_ref(service_id=self.service_id) + endpoint_ref = unit.new_endpoint_ref(service_id=self.service_id, + interface='public', + region_id=self.region_id) r = self.post('/endpoints', body={'endpoint': endpoint_ref}) return r.result['endpoint']['id'] @@ -429,13 +570,11 @@ class EndpointFilterTokenRequestTestCase(TestExtensionCase): self.put('/OS-EP-FILTER/projects/%(project_id)s' '/endpoints/%(endpoint_id)s' % { 'project_id': self.project['id'], - 'endpoint_id': endpoint_id1}, - expected_status=204) + 'endpoint_id': endpoint_id1}) self.put('/OS-EP-FILTER/projects/%(project_id)s' '/endpoints/%(endpoint_id)s' % { 'project_id': self.project['id'], - 'endpoint_id': endpoint_id2}, - expected_status=204) + 'endpoint_id': endpoint_id2}) # there should be only two endpoints in token catalog auth_data = self.build_authentication_request( @@ -454,8 +593,7 @@ class EndpointFilterTokenRequestTestCase(TestExtensionCase): self.put('/OS-EP-FILTER/projects/%(project_id)s' '/endpoints/%(endpoint_id)s' % { 'project_id': self.project['id'], - 'endpoint_id': self.endpoint_id}, - expected_status=204) + 'endpoint_id': self.endpoint_id}) auth_data = self.build_authentication_request( user_id=self.user['id'], @@ -474,7 +612,7 @@ class EndpointFilterTokenRequestTestCase(TestExtensionCase): auth_catalog.result['catalog']) -class JsonHomeTests(TestExtensionCase, test_v3.JsonHomeTestMixin): +class JsonHomeTests(EndpointFilterTestCase, test_v3.JsonHomeTestMixin): JSON_HOME_DATA = { 'http://docs.openstack.org/api/openstack-identity/3/ext/OS-EP-FILTER/' '1.0/rel/endpoint_projects': { @@ -545,7 +683,7 @@ class JsonHomeTests(TestExtensionCase, test_v3.JsonHomeTestMixin): } -class EndpointGroupCRUDTestCase(TestExtensionCase): +class EndpointGroupCRUDTestCase(EndpointFilterTestCase): DEFAULT_ENDPOINT_GROUP_BODY = { 'endpoint_group': { @@ -638,7 +776,7 @@ class EndpointGroupCRUDTestCase(TestExtensionCase): self.DEFAULT_ENDPOINT_GROUP_URL, self.DEFAULT_ENDPOINT_GROUP_BODY) url = '/OS-EP-FILTER/endpoint_groups/%(endpoint_group_id)s' % { 'endpoint_group_id': endpoint_group_id} - self.head(url, expected_status=200) + self.head(url, expected_status=http_client.OK) def test_check_invalid_endpoint_group(self): """HEAD /OS-EP-FILTER/endpoint_groups/{endpoint_group_id} @@ -832,7 +970,7 @@ class EndpointGroupCRUDTestCase(TestExtensionCase): self.project_id) url = self._get_project_endpoint_group_url( endpoint_group_id, self.project_id) - self.head(url, expected_status=200) + self.head(url, expected_status=http_client.OK) def test_check_endpoint_group_to_project_with_invalid_project_id(self): """Test HEAD with an invalid endpoint group and project association.""" @@ -891,7 +1029,7 @@ class EndpointGroupCRUDTestCase(TestExtensionCase): """ # create a service - service_ref = self.new_service_ref() + service_ref = unit.new_service_ref() response = self.post( '/services', body={'service': service_ref}) @@ -899,10 +1037,10 @@ class EndpointGroupCRUDTestCase(TestExtensionCase): service_id = response.result['service']['id'] # create an endpoint - endpoint_ref = self.new_endpoint_ref(service_id=service_id) - response = self.post( - '/endpoints', - body={'endpoint': endpoint_ref}) + endpoint_ref = unit.new_endpoint_ref(service_id=service_id, + interface='public', + region_id=self.region_id) + response = self.post('/endpoints', body={'endpoint': endpoint_ref}) endpoint_id = response.result['endpoint']['id'] # create an endpoint group @@ -929,7 +1067,7 @@ class EndpointGroupCRUDTestCase(TestExtensionCase): """ # create a temporary service - service_ref = self.new_service_ref() + service_ref = unit.new_service_ref() response = self.post('/services', body={'service': service_ref}) service_id2 = response.result['service']['id'] @@ -957,7 +1095,16 @@ class EndpointGroupCRUDTestCase(TestExtensionCase): 'project_id': self.default_domain_project_id} r = self.get(endpoints_url) endpoints = self.assertValidEndpointListResponse(r) - self.assertEqual(len(endpoints), 2) + self.assertEqual(2, len(endpoints)) + + # Ensure catalog includes the endpoints from endpoint_group project + # association, this is needed when a project scoped token is issued + # and "endpoint_filter.sql" backend driver is in place. + user_id = uuid.uuid4().hex + catalog_list = self.catalog_api.get_v3_catalog( + user_id, + self.default_domain_project_id) + self.assertEqual(2, len(catalog_list)) # Now remove project endpoint group association url = self._get_project_endpoint_group_url( @@ -971,7 +1118,12 @@ class EndpointGroupCRUDTestCase(TestExtensionCase): r = self.get(endpoints_url) endpoints = self.assertValidEndpointListResponse(r) - self.assertEqual(len(endpoints), 1) + self.assertEqual(1, len(endpoints)) + + catalog_list = self.catalog_api.get_v3_catalog( + user_id, + self.default_domain_project_id) + self.assertEqual(1, len(catalog_list)) def test_endpoint_group_project_cleanup_with_project(self): # create endpoint group @@ -979,7 +1131,7 @@ class EndpointGroupCRUDTestCase(TestExtensionCase): self.DEFAULT_ENDPOINT_GROUP_URL, self.DEFAULT_ENDPOINT_GROUP_BODY) # create new project and associate with endpoint_group - project_ref = self.new_project_ref(domain_id=self.domain_id) + project_ref = unit.new_project_ref(domain_id=self.domain_id) r = self.post('/projects', body={'project': project_ref}) project = self.assertValidProjectResponse(r, project_ref) url = self._get_project_endpoint_group_url(endpoint_group_id, @@ -1001,7 +1153,7 @@ class EndpointGroupCRUDTestCase(TestExtensionCase): self.DEFAULT_ENDPOINT_GROUP_URL, self.DEFAULT_ENDPOINT_GROUP_BODY) # create new project and associate with endpoint_group - project_ref = self.new_project_ref(domain_id=self.domain_id) + project_ref = unit.new_project_ref(domain_id=self.domain_id) r = self.post('/projects', body={'project': project_ref}) project = self.assertValidProjectResponse(r, project_ref) url = self._get_project_endpoint_group_url(endpoint_group_id, @@ -1049,6 +1201,153 @@ class EndpointGroupCRUDTestCase(TestExtensionCase): self.get(project_endpoint_group_url, expected_status=http_client.NOT_FOUND) + @unit.skip_if_cache_disabled('catalog') + def test_add_endpoint_group_to_project_invalidates_catalog_cache(self): + # create another endpoint with 'admin' interface which matches + # 'filters' definition in endpoint group, then there should be two + # endpoints returned when retrieving v3 catalog if cache works as + # expected. + # this should be done at first since `create_endpoint` will also + # invalidate cache. + endpoint_id2 = uuid.uuid4().hex + endpoint2 = unit.new_endpoint_ref(service_id=self.service_id, + region_id=self.region_id, + interface='admin', + id=endpoint_id2) + self.catalog_api.create_endpoint(endpoint_id2, endpoint2) + + # create a project and endpoint association. + self.put(self.default_request_url) + + # there is only one endpoint associated with the default project. + user_id = uuid.uuid4().hex + catalog = self.catalog_api.get_v3_catalog( + user_id, + self.default_domain_project_id) + + self.assertThat(catalog[0]['endpoints'], matchers.HasLength(1)) + + # create an endpoint group. + endpoint_group_id = self._create_valid_endpoint_group( + self.DEFAULT_ENDPOINT_GROUP_URL, self.DEFAULT_ENDPOINT_GROUP_BODY) + + # add the endpoint group to default project, bypassing + # catalog_api API manager. + self.catalog_api.driver.add_endpoint_group_to_project( + endpoint_group_id, + self.default_domain_project_id) + + # can get back only one endpoint from the cache, since the catalog + # is pulled out from cache. + invalid_catalog = self.catalog_api.get_v3_catalog( + user_id, + self.default_domain_project_id) + + self.assertThat(invalid_catalog[0]['endpoints'], + matchers.HasLength(1)) + self.assertEqual(catalog, invalid_catalog) + + # remove the endpoint group from default project, and add it again via + # catalog_api API manager. + self.catalog_api.driver.remove_endpoint_group_from_project( + endpoint_group_id, + self.default_domain_project_id) + + # add the endpoint group to default project. + self.catalog_api.add_endpoint_group_to_project( + endpoint_group_id, + self.default_domain_project_id) + + catalog = self.catalog_api.get_v3_catalog( + user_id, + self.default_domain_project_id) + + # now, it will return 2 endpoints since the cache has been + # invalidated. + self.assertThat(catalog[0]['endpoints'], matchers.HasLength(2)) + + ep_id_list = [catalog[0]['endpoints'][0]['id'], + catalog[0]['endpoints'][1]['id']] + self.assertItemsEqual([self.endpoint_id, endpoint_id2], ep_id_list) + + @unit.skip_if_cache_disabled('catalog') + def test_remove_endpoint_group_from_project_invalidates_cache(self): + # create another endpoint with 'admin' interface which matches + # 'filters' definition in endpoint group, then there should be two + # endpoints returned when retrieving v3 catalog. But only one + # endpoint will return after the endpoint group's deletion if cache + # works as expected. + # this should be done at first since `create_endpoint` will also + # invalidate cache. + endpoint_id2 = uuid.uuid4().hex + endpoint2 = unit.new_endpoint_ref(service_id=self.service_id, + region_id=self.region_id, + interface='admin', + id=endpoint_id2) + self.catalog_api.create_endpoint(endpoint_id2, endpoint2) + + # create project and endpoint association. + self.put(self.default_request_url) + + # create an endpoint group. + endpoint_group_id = self._create_valid_endpoint_group( + self.DEFAULT_ENDPOINT_GROUP_URL, self.DEFAULT_ENDPOINT_GROUP_BODY) + + # add the endpoint group to default project. + self.catalog_api.add_endpoint_group_to_project( + endpoint_group_id, + self.default_domain_project_id) + + # should get back two endpoints, one from endpoint project + # association, the other one is from endpoint_group project + # association. + user_id = uuid.uuid4().hex + catalog = self.catalog_api.get_v3_catalog( + user_id, + self.default_domain_project_id) + + self.assertThat(catalog[0]['endpoints'], matchers.HasLength(2)) + + ep_id_list = [catalog[0]['endpoints'][0]['id'], + catalog[0]['endpoints'][1]['id']] + self.assertItemsEqual([self.endpoint_id, endpoint_id2], ep_id_list) + + # remove endpoint_group project association, bypassing + # catalog_api API manager. + self.catalog_api.driver.remove_endpoint_group_from_project( + endpoint_group_id, + self.default_domain_project_id) + + # still get back two endpoints, since the catalog is pulled out + # from cache and the cache haven't been invalidated. + invalid_catalog = self.catalog_api.get_v3_catalog( + user_id, + self.default_domain_project_id) + + self.assertThat(invalid_catalog[0]['endpoints'], + matchers.HasLength(2)) + self.assertEqual(catalog, invalid_catalog) + + # add back the endpoint_group project association and remove it from + # manager. + self.catalog_api.driver.add_endpoint_group_to_project( + endpoint_group_id, + self.default_domain_project_id) + + self.catalog_api.remove_endpoint_group_from_project( + endpoint_group_id, + self.default_domain_project_id) + + # should only get back one endpoint since the cache has been + # invalidated after the endpoint_group project association was + # removed. + catalog = self.catalog_api.get_v3_catalog( + user_id, + self.default_domain_project_id) + + self.assertThat(catalog[0]['endpoints'], matchers.HasLength(1)) + self.assertEqual(self.endpoint_id, catalog[0]['endpoints'][0]['id']) + def _create_valid_endpoint_group(self, url, body): r = self.post(url, body=body) return r.result['endpoint_group']['id'] @@ -1072,13 +1371,15 @@ class EndpointGroupCRUDTestCase(TestExtensionCase): """Creates an endpoint associated with service and project.""" if not service_id: # create a new service - service_ref = self.new_service_ref() + service_ref = unit.new_service_ref() response = self.post( '/services', body={'service': service_ref}) service_id = response.result['service']['id'] # create endpoint - endpoint_ref = self.new_endpoint_ref(service_id=service_id) + endpoint_ref = unit.new_endpoint_ref(service_id=service_id, + interface='public', + region_id=self.region_id) response = self.post('/endpoints', body={'endpoint': endpoint_ref}) endpoint = response.result['endpoint'] diff --git a/keystone-moon/keystone/tests/unit/test_auth.py b/keystone-moon/keystone/tests/unit/test_auth.py index 6dd52c8a..6f44b316 100644 --- a/keystone-moon/keystone/tests/unit/test_auth.py +++ b/keystone-moon/keystone/tests/unit/test_auth.py @@ -14,6 +14,8 @@ import copy import datetime +import random +import string import uuid import mock @@ -26,11 +28,12 @@ from testtools import matchers from keystone import assignment from keystone import auth from keystone.common import authorization -from keystone import config +from keystone.common import config from keystone import exception from keystone.models import token_model from keystone.tests import unit from keystone.tests.unit import default_fixtures +from keystone.tests.unit import ksfixtures from keystone.tests.unit.ksfixtures import database from keystone import token from keystone.token import provider @@ -39,9 +42,10 @@ from keystone import trust CONF = cfg.CONF TIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ' -DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id -HOST_URL = 'http://keystone:5001' +HOST = ''.join(random.choice(string.ascii_lowercase) for x in range( + random.randint(5, 15))) +HOST_URL = 'http://%s' % (HOST) def _build_user_auth(token=None, user_id=None, username=None, @@ -127,9 +131,7 @@ class AuthBadRequests(AuthTest): context={}, auth={}) def test_empty_remote_user(self): - """Verify that _authenticate_external() raises exception if - REMOTE_USER is set as the empty string. - """ + """Verify exception is raised when REMOTE_USER is an empty string.""" context = {'environment': {'REMOTE_USER': ''}} self.assertRaises( token.controllers.ExternalAuthNotApplicable, @@ -223,6 +225,36 @@ class AuthBadRequests(AuthTest): self.controller.authenticate, {}, body_dict) + def test_authenticate_fails_if_project_unsafe(self): + """Verify authenticate to a project with unsafe name fails.""" + # Start with url name restrictions off, so we can create the unsafe + # named project + self.config_fixture.config(group='resource', + project_name_url_safe='off') + unsafe_name = 'i am not / safe' + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id, name=unsafe_name) + self.resource_api.create_project(project['id'], project) + self.assignment_api.add_role_to_user_and_project( + self.user_foo['id'], project['id'], self.role_member['id']) + no_context = {} + + body_dict = _build_user_auth( + username=self.user_foo['name'], + password=self.user_foo['password'], + tenant_name=project['name']) + + # Since name url restriction is off, we should be able to autenticate + self.controller.authenticate(no_context, body_dict) + + # Set the name url restriction to strict and we should fail to + # authenticate + self.config_fixture.config(group='resource', + project_name_url_safe='strict') + self.assertRaises(exception.Unauthorized, + self.controller.authenticate, + no_context, body_dict) + class AuthWithToken(AuthTest): def test_unscoped_token(self): @@ -286,7 +318,7 @@ class AuthWithToken(AuthTest): def test_auth_scoped_token_bad_project_with_debug(self): """Authenticating with an invalid project fails.""" - # Bug 1379952 reports poor user feedback, even in debug mode, + # Bug 1379952 reports poor user feedback, even in insecure_debug mode, # when the user accidentally passes a project name as an ID. # This test intentionally does exactly that. body_dict = _build_user_auth( @@ -294,8 +326,8 @@ class AuthWithToken(AuthTest): password=self.user_foo['password'], tenant_id=self.tenant_bar['name']) - # with debug enabled, this produces a friendly exception. - self.config_fixture.config(debug=True) + # with insecure_debug enabled, this produces a friendly exception. + self.config_fixture.config(debug=True, insecure_debug=True) e = self.assertRaises( exception.Unauthorized, self.controller.authenticate, @@ -308,7 +340,7 @@ class AuthWithToken(AuthTest): def test_auth_scoped_token_bad_project_without_debug(self): """Authenticating with an invalid project fails.""" - # Bug 1379952 reports poor user feedback, even in debug mode, + # Bug 1379952 reports poor user feedback, even in insecure_debug mode, # when the user accidentally passes a project name as an ID. # This test intentionally does exactly that. body_dict = _build_user_auth( @@ -316,8 +348,8 @@ class AuthWithToken(AuthTest): password=self.user_foo['password'], tenant_id=self.tenant_bar['name']) - # with debug disabled, authentication failure details are suppressed. - self.config_fixture.config(debug=False) + # with insecure_debug disabled (the default), authentication failure + # details are suppressed. e = self.assertRaises( exception.Unauthorized, self.controller.authenticate, @@ -336,9 +368,9 @@ class AuthWithToken(AuthTest): self.tenant_bar['id'], self.role_member['id']) # Now create a group role for this user as well - domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + domain1 = unit.new_domain_ref() self.resource_api.create_domain(domain1['id'], domain1) - new_group = {'domain_id': domain1['id'], 'name': uuid.uuid4().hex} + new_group = unit.new_group_ref(domain_id=domain1['id']) new_group = self.identity_api.create_group(new_group) self.identity_api.add_user_to_group(self.user_foo['id'], new_group['id']) @@ -428,10 +460,10 @@ class AuthWithToken(AuthTest): def test_deleting_role_revokes_token(self): role_controller = assignment.controllers.Role() - project1 = {'id': 'Project1', 'name': uuid.uuid4().hex, - 'domain_id': DEFAULT_DOMAIN_ID} + project1 = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) self.resource_api.create_project(project1['id'], project1) - role_one = {'id': 'role_one', 'name': uuid.uuid4().hex} + role_one = unit.new_role_ref(id='role_one') self.role_api.create_role(role_one['id'], role_one) self.assignment_api.add_role_to_user_and_project( self.user_foo['id'], project1['id'], role_one['id']) @@ -464,12 +496,10 @@ class AuthWithToken(AuthTest): no_context = {} admin_context = dict(is_admin=True, query_string={}) - project = { - 'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex, - 'domain_id': DEFAULT_DOMAIN_ID} + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) self.resource_api.create_project(project['id'], project) - role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + role = unit.new_role_ref() self.role_api.create_role(role['id'], role) self.assignment_api.add_role_to_user_and_project( self.user_foo['id'], project['id'], role['id']) @@ -642,6 +672,27 @@ class AuthWithToken(AuthTest): token_id=token_2_id) +class FernetAuthWithToken(AuthWithToken): + def config_overrides(self): + super(FernetAuthWithToken, self).config_overrides() + self.config_fixture.config(group='token', provider='fernet') + self.useFixture(ksfixtures.KeyRepository(self.config_fixture)) + + def test_token_auth_with_binding(self): + self.config_fixture.config(group='token', bind=['kerberos']) + body_dict = _build_user_auth() + self.assertRaises(exception.NotImplemented, + self.controller.authenticate, + self.context_with_remote_user, + body_dict) + + def test_revoke_with_no_audit_info(self): + self.skipTest('Fernet with v2.0 and revocation is broken') + + def test_deleting_role_revokes_token(self): + self.skipTest('Fernet with v2.0 and revocation is broken') + + class AuthWithPasswordCredentials(AuthTest): def test_auth_invalid_user(self): """Verify exception is raised if invalid user.""" @@ -682,7 +733,7 @@ class AuthWithPasswordCredentials(AuthTest): {}, body_dict) def test_authenticate_blank_password_credentials(self): - """Sending empty dict as passwordCredentials raises a 400 error.""" + """Sending empty dict as passwordCredentials raises 400 Bad Requset.""" body_dict = {'passwordCredentials': {}, 'tenantName': 'demo'} self.assertRaises(exception.ValidationError, self.controller.authenticate, @@ -708,27 +759,16 @@ class AuthWithPasswordCredentials(AuthTest): # user in auth data is from the new default domain. # 1) Create a new domain. - new_domain_id = uuid.uuid4().hex - new_domain = { - 'description': uuid.uuid4().hex, - 'enabled': True, - 'id': new_domain_id, - 'name': uuid.uuid4().hex, - } + new_domain = unit.new_domain_ref() + new_domain_id = new_domain['id'] self.resource_api.create_domain(new_domain_id, new_domain) # 2) Create user "foo" in new domain with different password than # default-domain foo. - new_user_password = uuid.uuid4().hex - new_user = { - 'name': self.user_foo['name'], - 'domain_id': new_domain_id, - 'password': new_user_password, - 'email': 'foo@bar2.com', - } - - new_user = self.identity_api.create_user(new_user) + new_user = unit.create_user(self.identity_api, + name=self.user_foo['name'], + domain_id=new_domain_id) # 3) Update the default_domain_id config option to the new domain @@ -739,7 +779,7 @@ class AuthWithPasswordCredentials(AuthTest): body_dict = _build_user_auth( username=self.user_foo['name'], - password=new_user_password) + password=new_user['password']) # The test is successful if this doesn't raise, so no need to assert. self.controller.authenticate({}, body_dict) @@ -856,7 +896,16 @@ class AuthWithTrust(AuthTest): token_id=token_id, token_data=self.token_provider_api.validate_token(token_id)) auth_context = authorization.token_to_auth_context(token_ref) - return {'environment': {authorization.AUTH_CONTEXT_ENV: auth_context}, + # NOTE(gyee): if public_endpoint and admin_endpoint are not set, which + # is the default, the base url will be constructed from the environment + # variables wsgi.url_scheme, SERVER_NAME, SERVER_PORT, and SCRIPT_NAME. + # We have to set them in the context so the base url can be constructed + # accordingly. + return {'environment': {authorization.AUTH_CONTEXT_ENV: auth_context, + 'wsgi.url_scheme': 'http', + 'SCRIPT_NAME': '/v3', + 'SERVER_PORT': '80', + 'SERVER_NAME': HOST}, 'token_id': token_id, 'host_url': HOST_URL} @@ -945,8 +994,9 @@ class AuthWithTrust(AuthTest): expires_at="2010-06-04T08:44:31.999999Z") def test_create_trust_without_project_id(self): - """Verify that trust can be created without project id and - token can be generated with that trust. + """Verify that trust can be created without project id. + + Also, token can be generated with that trust. """ unscoped_token = self.get_unscoped_token(self.trustor['name']) context = self._create_auth_context( @@ -977,9 +1027,7 @@ class AuthWithTrust(AuthTest): self.assertIn(role['id'], role_ids) def test_get_trust_without_auth_context(self): - """Verify that a trust cannot be retrieved when the auth context is - missing. - """ + """Verify a trust cannot be retrieved if auth context is missing.""" unscoped_token = self.get_unscoped_token(self.trustor['name']) context = self._create_auth_context( unscoped_token['access']['token']['id']) @@ -1001,8 +1049,6 @@ class AuthWithTrust(AuthTest): token_user = auth_response['access']['user'] self.assertEqual(token_user['id'], new_trust['trustee_user_id']) - # TODO(ayoung): Endpoints - def test_create_trust_impersonation(self): new_trust = self.create_trust(self.sample_data, self.trustor['name']) self.assertEqual(self.trustor['id'], new_trust['trustor_user_id']) @@ -1131,7 +1177,7 @@ class AuthWithTrust(AuthTest): request_body = _build_user_auth(token={'id': trust_token_id}, tenant_id=self.tenant_bar['id']) self.assertRaises( - exception.Forbidden, + exception.Unauthorized, self.controller.authenticate, {}, request_body) def test_delete_trust_revokes_token(self): @@ -1211,35 +1257,6 @@ class AuthWithTrust(AuthTest): new_trust['id'])['trust'] self.assertEqual(3, trust['remaining_uses']) - def test_v2_trust_token_contains_trustor_user_id_and_impersonation(self): - new_trust = self.create_trust(self.sample_data, self.trustor['name']) - auth_response = self.fetch_v2_token_from_trust(new_trust) - - self.assertEqual(new_trust['trustee_user_id'], - auth_response['access']['trust']['trustee_user_id']) - self.assertEqual(new_trust['trustor_user_id'], - auth_response['access']['trust']['trustor_user_id']) - self.assertEqual(new_trust['impersonation'], - auth_response['access']['trust']['impersonation']) - self.assertEqual(new_trust['id'], - auth_response['access']['trust']['id']) - - validate_response = self.controller.validate_token( - context=dict(is_admin=True, query_string={}), - token_id=auth_response['access']['token']['id']) - self.assertEqual( - new_trust['trustee_user_id'], - validate_response['access']['trust']['trustee_user_id']) - self.assertEqual( - new_trust['trustor_user_id'], - validate_response['access']['trust']['trustor_user_id']) - self.assertEqual( - new_trust['impersonation'], - validate_response['access']['trust']['impersonation']) - self.assertEqual( - new_trust['id'], - validate_response['access']['trust']['id']) - def disable_user(self, user): user['enabled'] = False self.identity_api.update_user(user['id'], user) @@ -1328,34 +1345,21 @@ class AuthCatalog(unit.SQLDriverOverrides, AuthTest): def _create_endpoints(self): def create_region(**kwargs): - ref = {'id': uuid.uuid4().hex} - ref.update(kwargs) + ref = unit.new_region_ref(**kwargs) self.catalog_api.create_region(ref) return ref def create_endpoint(service_id, region, **kwargs): - id_ = uuid.uuid4().hex - ref = { - 'id': id_, - 'interface': 'public', - 'region_id': region, - 'service_id': service_id, - 'url': 'http://localhost/%s' % uuid.uuid4().hex, - } - ref.update(kwargs) - self.catalog_api.create_endpoint(id_, ref) - return ref + endpoint = unit.new_endpoint_ref(region_id=region, + service_id=service_id, **kwargs) + + self.catalog_api.create_endpoint(endpoint['id'], endpoint) + return endpoint # Create a service for use with the endpoints. def create_service(**kwargs): - id_ = uuid.uuid4().hex - ref = { - 'id': id_, - 'name': uuid.uuid4().hex, - 'type': uuid.uuid4().hex, - } - ref.update(kwargs) - self.catalog_api.create_service(id_, ref) + ref = unit.new_service_ref(**kwargs) + self.catalog_api.create_service(ref['id'], ref) return ref enabled_service_ref = create_service(enabled=True) diff --git a/keystone-moon/keystone/tests/unit/test_auth_plugin.py b/keystone-moon/keystone/tests/unit/test_auth_plugin.py index 8dd22aa8..f0862ed6 100644 --- a/keystone-moon/keystone/tests/unit/test_auth_plugin.py +++ b/keystone-moon/keystone/tests/unit/test_auth_plugin.py @@ -183,7 +183,7 @@ class TestMapped(unit.TestCase): # make sure Mapped plugin got invoked with the correct payload ((context, auth_payload, auth_context), kwargs) = authenticate.call_args - self.assertEqual(auth_payload['protocol'], method_name) + self.assertEqual(method_name, auth_payload['protocol']) def test_supporting_multiple_methods(self): for method_name in ['saml2', 'openid', 'x509']: diff --git a/keystone-moon/keystone/tests/unit/test_backend_endpoint_policy.py b/keystone-moon/keystone/tests/unit/test_backend_endpoint_policy.py index 6c2181aa..f72cad63 100644 --- a/keystone-moon/keystone/tests/unit/test_backend_endpoint_policy.py +++ b/keystone-moon/keystone/tests/unit/test_backend_endpoint_policy.py @@ -18,6 +18,7 @@ from six.moves import range from testtools import matchers from keystone import exception +from keystone.tests import unit class PolicyAssociationTests(object): @@ -51,11 +52,11 @@ class PolicyAssociationTests(object): 5 - region 2, Service 0 """ - def new_endpoint(region_id, service_id): - endpoint = {'id': uuid.uuid4().hex, 'interface': 'test', - 'region_id': region_id, 'service_id': service_id, - 'url': '/url'} + endpoint = unit.new_endpoint_ref(interface='test', + region_id=region_id, + service_id=service_id, + url='/url') self.endpoint.append(self.catalog_api.create_endpoint( endpoint['id'], endpoint)) @@ -63,18 +64,18 @@ class PolicyAssociationTests(object): self.endpoint = [] self.service = [] self.region = [] + + parent_region_id = None for i in range(3): - policy = {'id': uuid.uuid4().hex, 'type': uuid.uuid4().hex, - 'blob': {'data': uuid.uuid4().hex}} + policy = unit.new_policy_ref() self.policy.append(self.policy_api.create_policy(policy['id'], policy)) - service = {'id': uuid.uuid4().hex, 'type': uuid.uuid4().hex} + service = unit.new_service_ref() self.service.append(self.catalog_api.create_service(service['id'], service)) - region = {'id': uuid.uuid4().hex, 'description': uuid.uuid4().hex} - # Link the 3 regions together as a hierarchy, [0] at the top - if i != 0: - region['parent_region_id'] = self.region[i - 1]['id'] + region = unit.new_region_ref(parent_region_id=parent_region_id) + # Link the regions together as a hierarchy, [0] at the top + parent_region_id = region['id'] self.region.append(self.catalog_api.create_region(region)) new_endpoint(self.region[0]['id'], self.service[0]['id']) diff --git a/keystone-moon/keystone/tests/unit/test_backend_id_mapping_sql.py b/keystone-moon/keystone/tests/unit/test_backend_id_mapping_sql.py index 6b691e5a..e6635e18 100644 --- a/keystone-moon/keystone/tests/unit/test_backend_id_mapping_sql.py +++ b/keystone-moon/keystone/tests/unit/test_backend_id_mapping_sql.py @@ -19,6 +19,7 @@ from testtools import matchers from keystone.common import sql from keystone.identity.mapping_backends import mapping +from keystone.tests import unit from keystone.tests.unit import identity_mapping as mapping_sql from keystone.tests.unit import test_backend_sql @@ -42,9 +43,9 @@ class SqlIDMapping(test_backend_sql.SqlTests): def load_sample_data(self): self.addCleanup(self.clean_sample_data) - domainA = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + domainA = unit.new_domain_ref() self.domainA = self.resource_api.create_domain(domainA['id'], domainA) - domainB = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + domainB = unit.new_domain_ref() self.domainB = self.resource_api.create_domain(domainB['id'], domainB) def clean_sample_data(self): diff --git a/keystone-moon/keystone/tests/unit/test_backend_kvs.py b/keystone-moon/keystone/tests/unit/test_backend_kvs.py index 7406192a..36af1c36 100644 --- a/keystone-moon/keystone/tests/unit/test_backend_kvs.py +++ b/keystone-moon/keystone/tests/unit/test_backend_kvs.py @@ -14,20 +14,17 @@ import datetime import uuid -from oslo_config import cfg from oslo_utils import timeutils import six from keystone.common import utils from keystone import exception from keystone.tests import unit -from keystone.tests.unit import test_backend +from keystone.tests.unit.ksfixtures import database +from keystone.tests.unit.token import test_backends as token_tests -CONF = cfg.CONF - - -class KvsToken(unit.TestCase, test_backend.TokenTests): +class KvsToken(unit.TestCase, token_tests.TokenTests): def setUp(self): super(KvsToken, self).setUp() self.load_backends() @@ -103,64 +100,11 @@ class KvsToken(unit.TestCase, test_backend.TokenTests): self.assertEqual(expected_user_token_list, user_token_list) -class KvsCatalog(unit.TestCase, test_backend.CatalogTests): - def setUp(self): - super(KvsCatalog, self).setUp() - self.load_backends() - self._load_fake_catalog() - - def config_overrides(self): - super(KvsCatalog, self).config_overrides() - self.config_fixture.config(group='catalog', driver='kvs') - - def _load_fake_catalog(self): - self.catalog_foobar = self.catalog_api.driver._create_catalog( - 'foo', 'bar', - {'RegionFoo': {'service_bar': {'foo': 'bar'}}}) - - def test_get_catalog_404(self): - # FIXME(dolph): this test should be moved up to test_backend - # FIXME(dolph): exceptions should be UserNotFound and ProjectNotFound - self.assertRaises(exception.NotFound, - self.catalog_api.get_catalog, - uuid.uuid4().hex, - 'bar') - - self.assertRaises(exception.NotFound, - self.catalog_api.get_catalog, - 'foo', - uuid.uuid4().hex) - - def test_get_catalog(self): - catalog_ref = self.catalog_api.get_catalog('foo', 'bar') - self.assertDictEqual(catalog_ref, self.catalog_foobar) - - def test_get_catalog_endpoint_disabled(self): - # This test doesn't apply to KVS because with the KVS backend the - # application creates the catalog (including the endpoints) for each - # user and project. Whether endpoints are enabled or disabled isn't - # a consideration. - f = super(KvsCatalog, self).test_get_catalog_endpoint_disabled - self.assertRaises(exception.NotFound, f) - - def test_get_v3_catalog_endpoint_disabled(self): - # There's no need to have disabled endpoints in the kvs catalog. Those - # endpoints should just be removed from the store. This just tests - # what happens currently when the super impl is called. - f = super(KvsCatalog, self).test_get_v3_catalog_endpoint_disabled - self.assertRaises(exception.NotFound, f) - - def test_list_regions_filtered_by_parent_region_id(self): - self.skipTest('KVS backend does not support hints') - - def test_service_filtering(self): - self.skipTest("kvs backend doesn't support filtering") - - class KvsTokenCacheInvalidation(unit.TestCase, - test_backend.TokenCacheInvalidation): + token_tests.TokenCacheInvalidation): def setUp(self): super(KvsTokenCacheInvalidation, self).setUp() + self.useFixture(database.Database(self.sql_driver_version_overrides)) self.load_backends() self._create_test_data() diff --git a/keystone-moon/keystone/tests/unit/test_backend_ldap.py b/keystone-moon/keystone/tests/unit/test_backend_ldap.py index d96ec376..cf618633 100644 --- a/keystone-moon/keystone/tests/unit/test_backend_ldap.py +++ b/keystone-moon/keystone/tests/unit/test_backend_ldap.py @@ -20,11 +20,15 @@ import uuid import ldap import mock from oslo_config import cfg +from oslo_log import versionutils +from oslotest import mockpatch import pkg_resources +from six.moves import http_client from six.moves import range from testtools import matchers from keystone.common import cache +from keystone.common import driver_hints from keystone.common import ldap as common_ldap from keystone.common.ldap import core as common_ldap_core from keystone import exception @@ -32,11 +36,14 @@ from keystone import identity from keystone.identity.mapping_backends import mapping as map from keystone import resource from keystone.tests import unit +from keystone.tests.unit.assignment import test_backends as assignment_tests from keystone.tests.unit import default_fixtures +from keystone.tests.unit.identity import test_backends as identity_tests from keystone.tests.unit import identity_mapping as mapping_sql from keystone.tests.unit.ksfixtures import database from keystone.tests.unit.ksfixtures import ldapdb -from keystone.tests.unit import test_backend +from keystone.tests.unit.resource import test_backends as resource_tests +from keystone.tests.unit.utils import wip CONF = cfg.CONF @@ -115,7 +122,9 @@ def create_group_container(identity_api): ('ou', ['Groups'])]) -class BaseLDAPIdentity(test_backend.IdentityTests): +class BaseLDAPIdentity(identity_tests.IdentityTests, + assignment_tests.AssignmentTests, + resource_tests.ResourceTests): def setUp(self): super(BaseLDAPIdentity, self).setUp() @@ -123,6 +132,7 @@ class BaseLDAPIdentity(test_backend.IdentityTests): self.load_backends() self.load_fixtures(default_fixtures) + self.config_fixture.config(group='os_inherit', enabled=False) def _get_domain_fixture(self): """Domains in LDAP are read-only, so just return the static one.""" @@ -141,6 +151,13 @@ class BaseLDAPIdentity(test_backend.IdentityTests): config_files.append(unit.dirs.tests_conf('backend_ldap.conf')) return config_files + def new_user_ref(self, domain_id, project_id=None, **kwargs): + ref = unit.new_user_ref(domain_id=domain_id, project_id=project_id, + **kwargs) + if 'id' not in kwargs: + del ref['id'] + return ref + def get_user_enabled_vals(self, user): user_dn = ( self.identity_api.driver.user._id_to_dn_string(user['id'])) @@ -156,17 +173,13 @@ class BaseLDAPIdentity(test_backend.IdentityTests): return None def test_build_tree(self): - """Regression test for building the tree names - """ + """Regression test for building the tree names.""" user_api = identity.backends.ldap.UserApi(CONF) self.assertTrue(user_api) self.assertEqual("ou=Users,%s" % CONF.ldap.suffix, user_api.tree_dn) def test_configurable_allowed_user_actions(self): - user = {'name': u'fäké1', - 'password': u'fäképass1', - 'domain_id': CONF.identity.default_domain_id, - 'tenants': ['bar']} + user = self.new_user_ref(domain_id=CONF.identity.default_domain_id) user = self.identity_api.create_user(user) self.identity_api.get_user(user['id']) @@ -185,10 +198,7 @@ class BaseLDAPIdentity(test_backend.IdentityTests): driver.user.allow_update = False driver.user.allow_delete = False - user = {'name': u'fäké1', - 'password': u'fäképass1', - 'domain_id': CONF.identity.default_domain_id, - 'tenants': ['bar']} + user = self.new_user_ref(domain_id=CONF.identity.default_domain_id) self.assertRaises(exception.ForbiddenAction, self.identity_api.create_user, user) @@ -215,7 +225,7 @@ class BaseLDAPIdentity(test_backend.IdentityTests): def test_user_filter(self): user_ref = self.identity_api.get_user(self.user_foo['id']) self.user_foo.pop('password') - self.assertDictEqual(user_ref, self.user_foo) + self.assertDictEqual(self.user_foo, user_ref) driver = self.identity_api._select_identity_driver( user_ref['domain_id']) @@ -227,6 +237,20 @@ class BaseLDAPIdentity(test_backend.IdentityTests): self.identity_api.get_user, self.user_foo['id']) + def test_list_users_by_name_and_with_filter(self): + # confirm that the user is not exposed when it does not match the + # filter setting in conf even if it is requested by name in user list + hints = driver_hints.Hints() + hints.add_filter('name', self.user_foo['name']) + domain_id = self.user_foo['domain_id'] + driver = self.identity_api._select_identity_driver(domain_id) + driver.user.ldap_filter = ('(|(cn=%s)(cn=%s))' % + (self.user_sna['id'], self.user_two['id'])) + users = self.identity_api.list_users( + domain_scope=self._set_domain_scope(domain_id), + hints=hints) + self.assertEqual(0, len(users)) + def test_remove_role_grant_from_user_and_project(self): self.assignment_api.create_grant(user_id=self.user_foo['id'], project_id=self.tenant_baz['id'], @@ -234,7 +258,7 @@ class BaseLDAPIdentity(test_backend.IdentityTests): roles_ref = self.assignment_api.list_grants( user_id=self.user_foo['id'], project_id=self.tenant_baz['id']) - self.assertDictEqual(roles_ref[0], self.role_member) + self.assertDictEqual(self.role_member, roles_ref[0]) self.assignment_api.delete_grant(user_id=self.user_foo['id'], project_id=self.tenant_baz['id'], @@ -251,11 +275,9 @@ class BaseLDAPIdentity(test_backend.IdentityTests): def test_get_and_remove_role_grant_by_group_and_project(self): new_domain = self._get_domain_fixture() - new_group = {'domain_id': new_domain['id'], - 'name': uuid.uuid4().hex} + new_group = unit.new_group_ref(domain_id=new_domain['id']) new_group = self.identity_api.create_group(new_group) - new_user = {'name': 'new_user', 'enabled': True, - 'domain_id': new_domain['id']} + new_user = self.new_user_ref(domain_id=new_domain['id']) new_user = self.identity_api.create_user(new_user) self.identity_api.add_user_to_group(new_user['id'], new_group['id']) @@ -273,7 +295,7 @@ class BaseLDAPIdentity(test_backend.IdentityTests): group_id=new_group['id'], project_id=self.tenant_bar['id']) self.assertNotEmpty(roles_ref) - self.assertDictEqual(roles_ref[0], self.role_member) + self.assertDictEqual(self.role_member, roles_ref[0]) self.assignment_api.delete_grant(group_id=new_group['id'], project_id=self.tenant_bar['id'], @@ -289,7 +311,44 @@ class BaseLDAPIdentity(test_backend.IdentityTests): role_id='member') def test_get_and_remove_role_grant_by_group_and_domain(self): - self.skipTest('N/A: LDAP does not support multiple domains') + # TODO(henry-nash): We should really rewrite the tests in + # unit.resource.test_backends to be more flexible as to where the + # domains are sourced from, so that we would not need to override such + # tests here. This is raised as bug 1373865. + new_domain = self._get_domain_fixture() + new_group = unit.new_group_ref(domain_id=new_domain['id'],) + new_group = self.identity_api.create_group(new_group) + new_user = self.new_user_ref(domain_id=new_domain['id']) + new_user = self.identity_api.create_user(new_user) + self.identity_api.add_user_to_group(new_user['id'], + new_group['id']) + + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + domain_id=new_domain['id']) + self.assertEqual(0, len(roles_ref)) + + self.assignment_api.create_grant(group_id=new_group['id'], + domain_id=new_domain['id'], + role_id='member') + + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + domain_id=new_domain['id']) + self.assertDictEqual(self.role_member, roles_ref[0]) + + self.assignment_api.delete_grant(group_id=new_group['id'], + domain_id=new_domain['id'], + role_id='member') + roles_ref = self.assignment_api.list_grants( + group_id=new_group['id'], + domain_id=new_domain['id']) + self.assertEqual(0, len(roles_ref)) + self.assertRaises(exception.NotFound, + self.assignment_api.delete_grant, + group_id=new_group['id'], + domain_id=new_domain['id'], + role_id='member') def test_get_role_assignment_by_domain_not_found(self): self.skipTest('N/A: LDAP does not support multiple domains') @@ -327,10 +386,12 @@ class BaseLDAPIdentity(test_backend.IdentityTests): def test_delete_group_with_user_project_domain_links(self): self.skipTest('N/A: LDAP does not support multiple domains') + def test_list_role_assignment_containing_names(self): + self.skipTest('N/A: LDAP does not support multiple domains') + def test_list_projects_for_user(self): domain = self._get_domain_fixture() - user1 = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, - 'domain_id': domain['id'], 'enabled': True} + user1 = self.new_user_ref(domain_id=domain['id']) user1 = self.identity_api.create_user(user1) user_projects = self.assignment_api.list_projects_for_user(user1['id']) self.assertThat(user_projects, matchers.HasLength(0)) @@ -347,11 +408,10 @@ class BaseLDAPIdentity(test_backend.IdentityTests): self.assertThat(user_projects, matchers.HasLength(2)) # Now, check number of projects through groups - user2 = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, - 'domain_id': domain['id'], 'enabled': True} + user2 = self.new_user_ref(domain_id=domain['id']) user2 = self.identity_api.create_user(user2) - group1 = {'name': uuid.uuid4().hex, 'domain_id': domain['id']} + group1 = unit.new_group_ref(domain_id=domain['id']) group1 = self.identity_api.create_group(group1) self.identity_api.add_user_to_group(user2['id'], group1['id']) @@ -377,12 +437,11 @@ class BaseLDAPIdentity(test_backend.IdentityTests): def test_list_projects_for_user_and_groups(self): domain = self._get_domain_fixture() # Create user1 - user1 = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, - 'domain_id': domain['id'], 'enabled': True} + user1 = self.new_user_ref(domain_id=domain['id']) user1 = self.identity_api.create_user(user1) # Create new group for user1 - group1 = {'name': uuid.uuid4().hex, 'domain_id': domain['id']} + group1 = unit.new_group_ref(domain_id=domain['id']) group1 = self.identity_api.create_group(group1) # Add user1 to group1 @@ -412,20 +471,17 @@ class BaseLDAPIdentity(test_backend.IdentityTests): def test_list_projects_for_user_with_grants(self): domain = self._get_domain_fixture() - new_user = {'name': 'new_user', 'password': uuid.uuid4().hex, - 'enabled': True, 'domain_id': domain['id']} + new_user = self.new_user_ref(domain_id=domain['id']) new_user = self.identity_api.create_user(new_user) - group1 = {'name': uuid.uuid4().hex, 'domain_id': domain['id']} + group1 = unit.new_group_ref(domain_id=domain['id']) group1 = self.identity_api.create_group(group1) - group2 = {'name': uuid.uuid4().hex, 'domain_id': domain['id']} + group2 = unit.new_group_ref(domain_id=domain['id']) group2 = self.identity_api.create_group(group2) - project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, - 'domain_id': domain['id']} + project1 = unit.new_project_ref(domain_id=domain['id']) self.resource_api.create_project(project1['id'], project1) - project2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, - 'domain_id': domain['id']} + project2 = unit.new_project_ref(domain_id=domain['id']) self.resource_api.create_project(project2['id'], project2) self.identity_api.add_user_to_group(new_user['id'], @@ -496,14 +552,11 @@ class BaseLDAPIdentity(test_backend.IdentityTests): def test_list_role_assignments_unfiltered(self): new_domain = self._get_domain_fixture() - new_user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, - 'enabled': True, 'domain_id': new_domain['id']} + new_user = self.new_user_ref(domain_id=new_domain['id']) new_user = self.identity_api.create_user(new_user) - new_group = {'domain_id': new_domain['id'], 'name': uuid.uuid4().hex} + new_group = unit.new_group_ref(domain_id=new_domain['id']) new_group = self.identity_api.create_group(new_group) - new_project = {'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex, - 'domain_id': new_domain['id']} + new_project = unit.new_project_ref(domain_id=new_domain['id']) self.resource_api.create_project(new_project['id'], new_project) # First check how many role grant already exist @@ -520,13 +573,6 @@ class BaseLDAPIdentity(test_backend.IdentityTests): after_assignments = len(self.assignment_api.list_role_assignments()) self.assertEqual(existing_assignments + 2, after_assignments) - def test_list_role_assignments_filtered_by_role(self): - # Domain roles are not supported by the LDAP Assignment backend - self.assertRaises( - exception.NotImplemented, - super(BaseLDAPIdentity, self). - test_list_role_assignments_filtered_by_role) - def test_list_role_assignments_dumb_member(self): self.config_fixture.config(group='ldap', use_dumb_member=True) self.ldapdb.clear() @@ -534,12 +580,9 @@ class BaseLDAPIdentity(test_backend.IdentityTests): self.load_fixtures(default_fixtures) new_domain = self._get_domain_fixture() - new_user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, - 'enabled': True, 'domain_id': new_domain['id']} + new_user = self.new_user_ref(domain_id=new_domain['id']) new_user = self.identity_api.create_user(new_user) - new_project = {'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex, - 'domain_id': new_domain['id']} + new_project = unit.new_project_ref(domain_id=new_domain['id']) self.resource_api.create_project(new_project['id'], new_project) self.assignment_api.create_grant(user_id=new_user['id'], project_id=new_project['id'], @@ -558,8 +601,7 @@ class BaseLDAPIdentity(test_backend.IdentityTests): self.load_backends() self.load_fixtures(default_fixtures) - user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, - 'enabled': True, 'domain_id': test_backend.DEFAULT_DOMAIN_ID} + user = self.new_user_ref(domain_id=CONF.identity.default_domain_id) user = self.identity_api.create_user(user) self.assignment_api.add_user_to_project(self.tenant_baz['id'], @@ -582,10 +624,8 @@ class BaseLDAPIdentity(test_backend.IdentityTests): are returned. """ - # Create a group - group = dict(name=uuid.uuid4().hex, - domain_id=CONF.identity.default_domain_id) + group = unit.new_group_ref(domain_id=CONF.identity.default_domain_id) group_id = self.identity_api.create_group(group)['id'] # Create a couple of users and add them to the group. @@ -617,10 +657,7 @@ class BaseLDAPIdentity(test_backend.IdentityTests): def test_list_group_members_when_no_members(self): # List group members when there is no member in the group. # No exception should be raised. - group = { - 'domain_id': CONF.identity.default_domain_id, - 'name': uuid.uuid4().hex, - 'description': uuid.uuid4().hex} + group = unit.new_group_ref(domain_id=CONF.identity.default_domain_id) group = self.identity_api.create_group(group) # If this doesn't raise, then the test is successful. @@ -633,8 +670,7 @@ class BaseLDAPIdentity(test_backend.IdentityTests): self.load_fixtures(default_fixtures) # Create a group - group = dict(name=uuid.uuid4().hex, - domain_id=CONF.identity.default_domain_id) + group = unit.new_group_ref(domain_id=CONF.identity.default_domain_id) group_id = self.identity_api.create_group(group)['id'] # Create a user @@ -651,30 +687,23 @@ class BaseLDAPIdentity(test_backend.IdentityTests): self.assertNotIn(dumb_id, user_ids) def test_list_domains(self): + # We have more domains here than the parent class, check for the + # correct number of domains for the multildap backend configs + domain1 = unit.new_domain_ref() + domain2 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + self.resource_api.create_domain(domain2['id'], domain2) domains = self.resource_api.list_domains() - self.assertEqual( - [resource.calc_default_domain()], - domains) - - def test_list_domains_non_default_domain_id(self): - # If change the default_domain_id, the ID of the default domain - # returned by list_domains changes is the new default_domain_id. - - new_domain_id = uuid.uuid4().hex - self.config_fixture.config(group='identity', - default_domain_id=new_domain_id) - - domains = self.resource_api.list_domains() - - self.assertEqual(new_domain_id, domains[0]['id']) + self.assertEqual(7, len(domains)) + domain_ids = [] + for domain in domains: + domain_ids.append(domain.get('id')) + self.assertIn(CONF.identity.default_domain_id, domain_ids) + self.assertIn(domain1['id'], domain_ids) + self.assertIn(domain2['id'], domain_ids) def test_authenticate_requires_simple_bind(self): - user = { - 'name': 'NO_META', - 'domain_id': test_backend.DEFAULT_DOMAIN_ID, - 'password': 'no_meta2', - 'enabled': True, - } + user = self.new_user_ref(domain_id=CONF.identity.default_domain_id) user = self.identity_api.create_user(user) self.assignment_api.add_user_to_project(self.tenant_baz['id'], user['id']) @@ -689,34 +718,54 @@ class BaseLDAPIdentity(test_backend.IdentityTests): user_id=user['id'], password=None) - # (spzala)The group and domain crud tests below override the standard ones - # in test_backend.py so that we can exclude the update name test, since we - # do not yet support the update of either group or domain names with LDAP. - # In the tests below, the update is demonstrated by updating description. - # Refer to bug 1136403 for more detail. - def test_group_crud(self): - group = { - 'domain_id': CONF.identity.default_domain_id, - 'name': uuid.uuid4().hex, - 'description': uuid.uuid4().hex} + # The group and domain CRUD tests below override the standard ones in + # unit.identity.test_backends.py so that we can exclude the update name + # test, since we do not (and will not) support the update of either group + # or domain names with LDAP. In the tests below, the update is tested by + # updating description. + @mock.patch.object(versionutils, 'report_deprecated_feature') + def test_group_crud(self, mock_deprecator): + # NOTE(stevemar): As of the Mitaka release, we now check for calls that + # the LDAP write functionality has been deprecated. + group = unit.new_group_ref(domain_id=CONF.identity.default_domain_id) group = self.identity_api.create_group(group) + args, _kwargs = mock_deprecator.call_args + self.assertIn("create_group for the LDAP identity backend", args[1]) + group_ref = self.identity_api.get_group(group['id']) - self.assertDictEqual(group_ref, group) + self.assertDictEqual(group, group_ref) group['description'] = uuid.uuid4().hex self.identity_api.update_group(group['id'], group) + args, _kwargs = mock_deprecator.call_args + self.assertIn("update_group for the LDAP identity backend", args[1]) + group_ref = self.identity_api.get_group(group['id']) - self.assertDictEqual(group_ref, group) + self.assertDictEqual(group, group_ref) self.identity_api.delete_group(group['id']) + args, _kwargs = mock_deprecator.call_args + self.assertIn("delete_group for the LDAP identity backend", args[1]) self.assertRaises(exception.GroupNotFound, self.identity_api.get_group, group['id']) + @mock.patch.object(versionutils, 'report_deprecated_feature') + def test_add_remove_user_group_deprecated(self, mock_deprecator): + group = unit.new_group_ref(domain_id=CONF.identity.default_domain_id) + group = self.identity_api.create_group(group) + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user = self.identity_api.create_user(user) + self.identity_api.add_user_to_group(user['id'], group['id']) + args, _kwargs = mock_deprecator.call_args + self.assertIn("add_user_to_group for the LDAP identity", args[1]) + + self.identity_api.remove_user_from_group(user['id'], group['id']) + args, _kwargs = mock_deprecator.call_args + self.assertIn("remove_user_from_group for the LDAP identity", args[1]) + @unit.skip_if_cache_disabled('identity') def test_cache_layer_group_crud(self): - group = { - 'domain_id': CONF.identity.default_domain_id, - 'name': uuid.uuid4().hex} + group = unit.new_group_ref(domain_id=CONF.identity.default_domain_id) group = self.identity_api.create_group(group) # cache the result group_ref = self.identity_api.get_group(group['id']) @@ -731,9 +780,7 @@ class BaseLDAPIdentity(test_backend.IdentityTests): self.assertRaises(exception.GroupNotFound, self.identity_api.get_group, group['id']) - group = { - 'domain_id': CONF.identity.default_domain_id, - 'name': uuid.uuid4().hex} + group = unit.new_group_ref(domain_id=CONF.identity.default_domain_id) group = self.identity_api.create_group(group) # cache the result self.identity_api.get_group(group['id']) @@ -749,11 +796,8 @@ class BaseLDAPIdentity(test_backend.IdentityTests): CONF.identity.default_domain_id) driver.user.attribute_ignore = ['enabled', 'email', 'tenants', 'tenantId'] - user = {'name': u'fäké1', - 'password': u'fäképass1', - 'domain_id': CONF.identity.default_domain_id, - 'default_project_id': 'maps_to_none', - } + user = self.new_user_ref(domain_id=CONF.identity.default_domain_id, + project_id='maps_to_none') # If this doesn't raise, then the test is successful. user = self.identity_api.create_user(user) @@ -765,9 +809,8 @@ class BaseLDAPIdentity(test_backend.IdentityTests): boolean_strings = ['TRUE', 'FALSE', 'true', 'false', 'True', 'False', 'TrUe' 'FaLse'] for name in boolean_strings: - user = { - 'name': name, - 'domain_id': CONF.identity.default_domain_id} + user = self.new_user_ref(name=name, + domain_id=CONF.identity.default_domain_id) user_ref = self.identity_api.create_user(user) user_info = self.identity_api.get_user(user_ref['id']) self.assertEqual(name, user_info['name']) @@ -786,10 +829,7 @@ class BaseLDAPIdentity(test_backend.IdentityTests): driver.user.attribute_ignore = ['enabled', 'email', 'tenants', 'tenantId'] - user = {'name': u'fäké1', - 'password': u'fäképass1', - 'domain_id': CONF.identity.default_domain_id, - } + user = self.new_user_ref(domain_id=CONF.identity.default_domain_id) user_ref = self.identity_api.create_user(user) @@ -818,19 +858,14 @@ class BaseLDAPIdentity(test_backend.IdentityTests): def test_user_id_comma(self): """Even if the user has a , in their ID, groups can be listed.""" - # Create a user with a , in their ID # NOTE(blk-u): the DN for this user is hard-coded in fakeldap! # Since we want to fake up this special ID, we'll squirt this # direct into the driver and bypass the manager layer. user_id = u'Doe, John' - user = { - 'id': user_id, - 'name': self.getUniqueString(), - 'password': self.getUniqueString(), - 'domain_id': CONF.identity.default_domain_id, - } + user = self.new_user_ref(id=user_id, + domain_id=CONF.identity.default_domain_id) user = self.identity_api.driver.create_user(user_id, user) # Now we'll use the manager to discover it, which will create a @@ -843,13 +878,8 @@ class BaseLDAPIdentity(test_backend.IdentityTests): break # Create a group - group_id = uuid.uuid4().hex - group = { - 'id': group_id, - 'name': self.getUniqueString(prefix='tuidc'), - 'description': self.getUniqueString(), - 'domain_id': CONF.identity.default_domain_id, - } + group = unit.new_group_ref(domain_id=CONF.identity.default_domain_id) + group_id = group['id'] group = self.identity_api.driver.create_group(group_id, group) # Now we'll use the manager to discover it, which will create a # Public ID for it. @@ -870,21 +900,15 @@ class BaseLDAPIdentity(test_backend.IdentityTests): self.assertThat(ref_list, matchers.Equals([group])) def test_user_id_comma_grants(self): - """Even if the user has a , in their ID, can get user and group grants. - """ - + """List user and group grants, even with a comma in the user's ID.""" # Create a user with a , in their ID # NOTE(blk-u): the DN for this user is hard-coded in fakeldap! # Since we want to fake up this special ID, we'll squirt this # direct into the driver and bypass the manager layer user_id = u'Doe, John' - user = { - 'id': user_id, - 'name': self.getUniqueString(), - 'password': self.getUniqueString(), - 'domain_id': CONF.identity.default_domain_id, - } + user = self.new_user_ref(id=user_id, + domain_id=CONF.identity.default_domain_id) self.identity_api.driver.create_user(user_id, user) # Now we'll use the manager to discover it, which will create a @@ -943,8 +967,7 @@ class BaseLDAPIdentity(test_backend.IdentityTests): # There's no group fixture so create a group. new_domain = self._get_domain_fixture() - new_group = {'domain_id': new_domain['id'], - 'name': uuid.uuid4().hex} + new_group = unit.new_group_ref(domain_id=new_domain['id']) new_group = self.identity_api.create_group(new_group) # Attempt to disable the group. @@ -959,39 +982,55 @@ class BaseLDAPIdentity(test_backend.IdentityTests): self.assertNotIn('enabled', group_info) def test_project_enabled_ignored_disable_error(self): - # When the server is configured so that the enabled attribute is - # ignored for projects, projects cannot be disabled. - - self.config_fixture.config(group='ldap', - project_attribute_ignore=['enabled']) - - # Need to re-load backends for the config change to take effect. - self.load_backends() - - # Attempt to disable the project. - self.assertRaises(exception.ForbiddenAction, - self.resource_api.update_project, - self.tenant_baz['id'], {'enabled': False}) - - project_info = self.resource_api.get_project(self.tenant_baz['id']) - - # Unlike other entities, if 'enabled' is ignored then 'enabled' is - # returned as part of the ref. - self.assertIs(True, project_info['enabled']) + self.skipTest('Resource LDAP has been removed') def test_list_role_assignment_by_domain(self): """Multiple domain assignments are not supported.""" self.assertRaises( - (exception.Forbidden, exception.DomainNotFound), + (exception.Forbidden, exception.DomainNotFound, + exception.ValidationError), super(BaseLDAPIdentity, self).test_list_role_assignment_by_domain) def test_list_role_assignment_by_user_with_domain_group_roles(self): """Multiple domain assignments are not supported.""" self.assertRaises( - (exception.Forbidden, exception.DomainNotFound), + (exception.Forbidden, exception.DomainNotFound, + exception.ValidationError), super(BaseLDAPIdentity, self). test_list_role_assignment_by_user_with_domain_group_roles) + def test_domain_crud(self): + self.skipTest('Resource LDAP has been removed') + + def test_list_role_assignment_using_sourced_groups_with_domains(self): + """Multiple domain assignments are not supported.""" + self.assertRaises( + (exception.Forbidden, exception.ValidationError, + exception.DomainNotFound), + super(BaseLDAPIdentity, self). + test_list_role_assignment_using_sourced_groups_with_domains) + + def test_create_project_with_domain_id_and_without_parent_id(self): + """Multiple domains are not supported.""" + self.assertRaises( + exception.ValidationError, + super(BaseLDAPIdentity, self). + test_create_project_with_domain_id_and_without_parent_id) + + def test_create_project_with_domain_id_mismatch_to_parent_domain(self): + """Multiple domains are not supported.""" + self.assertRaises( + exception.ValidationError, + super(BaseLDAPIdentity, self). + test_create_project_with_domain_id_mismatch_to_parent_domain) + + def test_remove_foreign_assignments_when_deleting_a_domain(self): + """Multiple domains are not supported.""" + self.assertRaises( + (exception.ValidationError, exception.DomainNotFound), + super(BaseLDAPIdentity, + self).test_remove_foreign_assignments_when_deleting_a_domain) + class LDAPIdentity(BaseLDAPIdentity, unit.TestCase): @@ -1002,46 +1041,46 @@ class LDAPIdentity(BaseLDAPIdentity, unit.TestCase): self.useFixture(database.Database()) super(LDAPIdentity, self).setUp() _assert_backends(self, - assignment='ldap', + assignment='sql', identity='ldap', - resource='ldap') + resource='sql') def load_fixtures(self, fixtures): # Override super impl since need to create group container. create_group_container(self.identity_api) super(LDAPIdentity, self).load_fixtures(fixtures) + def test_list_domains(self): + domains = self.resource_api.list_domains() + self.assertEqual([resource.calc_default_domain()], domains) + def test_configurable_allowed_project_actions(self): domain = self._get_domain_fixture() - tenant = {'id': u'fäké1', 'name': u'fäké1', 'enabled': True, - 'domain_id': domain['id']} - self.resource_api.create_project(u'fäké1', tenant) - tenant_ref = self.resource_api.get_project(u'fäké1') - self.assertEqual(u'fäké1', tenant_ref['id']) + project = unit.new_project_ref(domain_id=domain['id']) + project = self.resource_api.create_project(project['id'], project) + project_ref = self.resource_api.get_project(project['id']) + self.assertEqual(project['id'], project_ref['id']) - tenant['enabled'] = False - self.resource_api.update_project(u'fäké1', tenant) + project['enabled'] = False + self.resource_api.update_project(project['id'], project) - self.resource_api.delete_project(u'fäké1') + self.resource_api.delete_project(project['id']) self.assertRaises(exception.ProjectNotFound, self.resource_api.get_project, - u'fäké1') + project['id']) def test_configurable_subtree_delete(self): self.config_fixture.config(group='ldap', allow_subtree_delete=True) self.load_backends() - project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, - 'domain_id': CONF.identity.default_domain_id} + project1 = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) self.resource_api.create_project(project1['id'], project1) - role1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + role1 = unit.new_role_ref() self.role_api.create_role(role1['id'], role1) - user1 = {'name': uuid.uuid4().hex, - 'domain_id': CONF.identity.default_domain_id, - 'password': uuid.uuid4().hex, - 'enabled': True} + user1 = self.new_user_ref(domain_id=CONF.identity.default_domain_id) user1 = self.identity_api.create_user(user1) self.assignment_api.add_role_to_user_and_project( @@ -1062,48 +1101,10 @@ class LDAPIdentity(BaseLDAPIdentity, unit.TestCase): self.assertEqual(0, len(list)) def test_configurable_forbidden_project_actions(self): - self.config_fixture.config( - group='ldap', project_allow_create=False, - project_allow_update=False, project_allow_delete=False) - self.load_backends() - - domain = self._get_domain_fixture() - tenant = {'id': u'fäké1', 'name': u'fäké1', 'domain_id': domain['id']} - self.assertRaises(exception.ForbiddenAction, - self.resource_api.create_project, - u'fäké1', - tenant) - - self.tenant_bar['enabled'] = False - self.assertRaises(exception.ForbiddenAction, - self.resource_api.update_project, - self.tenant_bar['id'], - self.tenant_bar) - self.assertRaises(exception.ForbiddenAction, - self.resource_api.delete_project, - self.tenant_bar['id']) + self.skipTest('Resource LDAP has been removed') def test_project_filter(self): - tenant_ref = self.resource_api.get_project(self.tenant_bar['id']) - self.assertDictEqual(tenant_ref, self.tenant_bar) - - self.config_fixture.config(group='ldap', - project_filter='(CN=DOES_NOT_MATCH)') - self.load_backends() - # NOTE(morganfainberg): CONF.ldap.project_filter will not be - # dynamically changed at runtime. This invalidate is a work-around for - # the expectation that it is safe to change config values in tests that - # could affect what the drivers would return up to the manager. This - # solves this assumption when working with aggressive (on-create) - # cache population. - self.role_api.get_role.invalidate(self.role_api, - self.role_member['id']) - self.role_api.get_role(self.role_member['id']) - self.resource_api.get_project.invalidate(self.resource_api, - self.tenant_bar['id']) - self.assertRaises(exception.ProjectNotFound, - self.resource_api.get_project, - self.tenant_bar['id']) + self.skipTest('Resource LDAP has been removed') def test_dumb_member(self): self.config_fixture.config(group='ldap', use_dumb_member=True) @@ -1116,71 +1117,10 @@ class LDAPIdentity(BaseLDAPIdentity, unit.TestCase): dumb_id) def test_project_attribute_mapping(self): - self.config_fixture.config( - group='ldap', project_name_attribute='ou', - project_desc_attribute='description', - project_enabled_attribute='enabled') - self.ldapdb.clear() - self.load_backends() - self.load_fixtures(default_fixtures) - # NOTE(morganfainberg): CONF.ldap.project_name_attribute, - # CONF.ldap.project_desc_attribute, and - # CONF.ldap.project_enabled_attribute will not be - # dynamically changed at runtime. This invalidate is a work-around for - # the expectation that it is safe to change config values in tests that - # could affect what the drivers would return up to the manager. This - # solves this assumption when working with aggressive (on-create) - # cache population. - self.resource_api.get_project.invalidate(self.resource_api, - self.tenant_baz['id']) - tenant_ref = self.resource_api.get_project(self.tenant_baz['id']) - self.assertEqual(self.tenant_baz['id'], tenant_ref['id']) - self.assertEqual(self.tenant_baz['name'], tenant_ref['name']) - self.assertEqual( - self.tenant_baz['description'], - tenant_ref['description']) - self.assertEqual(self.tenant_baz['enabled'], tenant_ref['enabled']) - - self.config_fixture.config(group='ldap', - project_name_attribute='description', - project_desc_attribute='ou') - self.load_backends() - # NOTE(morganfainberg): CONF.ldap.project_name_attribute, - # CONF.ldap.project_desc_attribute, and - # CONF.ldap.project_enabled_attribute will not be - # dynamically changed at runtime. This invalidate is a work-around for - # the expectation that it is safe to change config values in tests that - # could affect what the drivers would return up to the manager. This - # solves this assumption when working with aggressive (on-create) - # cache population. - self.resource_api.get_project.invalidate(self.resource_api, - self.tenant_baz['id']) - tenant_ref = self.resource_api.get_project(self.tenant_baz['id']) - self.assertEqual(self.tenant_baz['id'], tenant_ref['id']) - self.assertEqual(self.tenant_baz['description'], tenant_ref['name']) - self.assertEqual(self.tenant_baz['name'], tenant_ref['description']) - self.assertEqual(self.tenant_baz['enabled'], tenant_ref['enabled']) + self.skipTest('Resource LDAP has been removed') def test_project_attribute_ignore(self): - self.config_fixture.config( - group='ldap', - project_attribute_ignore=['name', 'description', 'enabled']) - self.ldapdb.clear() - self.load_backends() - self.load_fixtures(default_fixtures) - # NOTE(morganfainberg): CONF.ldap.project_attribute_ignore will not be - # dynamically changed at runtime. This invalidate is a work-around for - # the expectation that it is safe to change configs values in tests - # that could affect what the drivers would return up to the manager. - # This solves this assumption when working with aggressive (on-create) - # cache population. - self.resource_api.get_project.invalidate(self.resource_api, - self.tenant_baz['id']) - tenant_ref = self.resource_api.get_project(self.tenant_baz['id']) - self.assertEqual(self.tenant_baz['id'], tenant_ref['id']) - self.assertNotIn('name', tenant_ref) - self.assertNotIn('description', tenant_ref) - self.assertNotIn('enabled', tenant_ref) + self.skipTest('Resource LDAP has been removed') def test_user_enable_attribute_mask(self): self.config_fixture.config(group='ldap', user_enabled_mask=2, @@ -1189,8 +1129,7 @@ class LDAPIdentity(BaseLDAPIdentity, unit.TestCase): self.load_backends() self.load_fixtures(default_fixtures) - user = {'name': u'fäké1', 'enabled': True, - 'domain_id': CONF.identity.default_domain_id} + user = self.new_user_ref(domain_id=CONF.identity.default_domain_id) user_ref = self.identity_api.create_user(user) @@ -1237,14 +1176,12 @@ class LDAPIdentity(BaseLDAPIdentity, unit.TestCase): self.load_backends() self.load_fixtures(default_fixtures) - user1 = {'name': u'fäké1', 'enabled': True, - 'domain_id': CONF.identity.default_domain_id} + user1 = self.new_user_ref(domain_id=CONF.identity.default_domain_id) - user2 = {'name': u'fäké2', 'enabled': False, - 'domain_id': CONF.identity.default_domain_id} + user2 = self.new_user_ref(enabled=False, + domain_id=CONF.identity.default_domain_id) - user3 = {'name': u'fäké3', - 'domain_id': CONF.identity.default_domain_id} + user3 = self.new_user_ref(domain_id=CONF.identity.default_domain_id) # Ensure that the LDAP attribute is False for a newly created # enabled user. @@ -1473,15 +1410,28 @@ class LDAPIdentity(BaseLDAPIdentity, unit.TestCase): group='ldap', user_additional_attribute_mapping=['description:name']) self.load_backends() - user = { - 'name': 'EXTRA_ATTRIBUTES', - 'password': 'extra', - 'domain_id': CONF.identity.default_domain_id - } + user = self.new_user_ref(name='EXTRA_ATTRIBUTES', + password='extra', + domain_id=CONF.identity.default_domain_id) user = self.identity_api.create_user(user) dn, attrs = self.identity_api.driver.user._ldap_get(user['id']) self.assertThat([user['name']], matchers.Equals(attrs['description'])) + def test_user_description_attribute_mapping(self): + self.config_fixture.config( + group='ldap', + user_description_attribute='displayName') + self.load_backends() + + user = self.new_user_ref(domain_id=CONF.identity.default_domain_id, + displayName=uuid.uuid4().hex) + description = user['displayName'] + user = self.identity_api.create_user(user) + res = self.identity_api.driver.user.get_all() + + new_user = [u for u in res if u['id'] == user['id']][0] + self.assertThat(new_user['description'], matchers.Equals(description)) + def test_user_extra_attribute_mapping_description_is_returned(self): # Given a mapping like description:description, the description is # returned. @@ -1491,13 +1441,9 @@ class LDAPIdentity(BaseLDAPIdentity, unit.TestCase): user_additional_attribute_mapping=['description:description']) self.load_backends() - description = uuid.uuid4().hex - user = { - 'name': uuid.uuid4().hex, - 'description': description, - 'password': uuid.uuid4().hex, - 'domain_id': CONF.identity.default_domain_id - } + user = self.new_user_ref(domain_id=CONF.identity.default_domain_id, + description=uuid.uuid4().hex) + description = user['description'] user = self.identity_api.create_user(user) res = self.identity_api.driver.user.get_all() @@ -1551,52 +1497,17 @@ class LDAPIdentity(BaseLDAPIdentity, unit.TestCase): 'fake': 'invalid', 'invalid2': ''} self.assertDictEqual(expected_dict, mapping) -# TODO(henry-nash): These need to be removed when the full LDAP implementation -# is submitted - see Bugs 1092187, 1101287, 1101276, 1101289 - - def test_domain_crud(self): - domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, - 'enabled': True, 'description': uuid.uuid4().hex} - self.assertRaises(exception.Forbidden, + def test_create_domain(self): + domain = unit.new_domain_ref() + self.assertRaises(exception.ValidationError, self.resource_api.create_domain, domain['id'], domain) - self.assertRaises(exception.Conflict, - self.resource_api.create_domain, - CONF.identity.default_domain_id, - domain) - self.assertRaises(exception.DomainNotFound, - self.resource_api.get_domain, - domain['id']) - - domain['description'] = uuid.uuid4().hex - self.assertRaises(exception.DomainNotFound, - self.resource_api.update_domain, - domain['id'], - domain) - self.assertRaises(exception.Forbidden, - self.resource_api.update_domain, - CONF.identity.default_domain_id, - domain) - self.assertRaises(exception.DomainNotFound, - self.resource_api.get_domain, - domain['id']) - self.assertRaises(exception.DomainNotFound, - self.resource_api.delete_domain, - domain['id']) - self.assertRaises(exception.Forbidden, - self.resource_api.delete_domain, - CONF.identity.default_domain_id) - self.assertRaises(exception.DomainNotFound, - self.resource_api.get_domain, - domain['id']) @unit.skip_if_no_multiple_domains_support def test_create_domain_case_sensitivity(self): # domains are read-only, so case sensitivity isn't an issue - ref = { - 'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex} + ref = unit.new_domain_ref() self.assertRaises(exception.Forbidden, self.resource_api.create_domain, ref['id'], @@ -1624,22 +1535,18 @@ class LDAPIdentity(BaseLDAPIdentity, unit.TestCase): # NOTE(topol): LDAP implementation does not currently support the # updating of a project name so this method override # provides a different update test - project = {'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex, - 'domain_id': CONF.identity.default_domain_id, - 'description': uuid.uuid4().hex, - 'enabled': True, - 'parent_id': None, - 'is_domain': False} - self.resource_api.create_project(project['id'], project) + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + + project = self.resource_api.create_project(project['id'], project) project_ref = self.resource_api.get_project(project['id']) - self.assertDictEqual(project_ref, project) + self.assertDictEqual(project, project_ref) project['description'] = uuid.uuid4().hex self.resource_api.update_project(project['id'], project) project_ref = self.resource_api.get_project(project['id']) - self.assertDictEqual(project_ref, project) + self.assertDictEqual(project, project_ref) self.resource_api.delete_project(project['id']) self.assertRaises(exception.ProjectNotFound, @@ -1651,12 +1558,11 @@ class LDAPIdentity(BaseLDAPIdentity, unit.TestCase): # NOTE(morganfainberg): LDAP implementation does not currently support # updating project names. This method override provides a different # update test. - project = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, - 'domain_id': CONF.identity.default_domain_id, - 'description': uuid.uuid4().hex} + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) project_id = project['id'] # Create a project - self.resource_api.create_project(project_id, project) + project = self.resource_api.create_project(project_id, project) self.resource_api.get_project(project_id) updated_project = copy.deepcopy(project) updated_project['description'] = uuid.uuid4().hex @@ -1700,70 +1606,10 @@ class LDAPIdentity(BaseLDAPIdentity, unit.TestCase): self.resource_api.get_project, project_id) - def _assert_create_hierarchy_not_allowed(self): - domain = self._get_domain_fixture() - - project1 = {'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex, - 'description': '', - 'domain_id': domain['id'], - 'enabled': True, - 'parent_id': None, - 'is_domain': False} - self.resource_api.create_project(project1['id'], project1) - - # Creating project2 under project1. LDAP will not allow - # the creation of a project with parent_id being set - project2 = {'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex, - 'description': '', - 'domain_id': domain['id'], - 'enabled': True, - 'parent_id': project1['id'], - 'is_domain': False} - - self.assertRaises(exception.InvalidParentProject, - self.resource_api.create_project, - project2['id'], - project2) - - # Now, we'll create project 2 with no parent - project2['parent_id'] = None - self.resource_api.create_project(project2['id'], project2) - - # Returning projects to be used across the tests - return [project1, project2] - - def _assert_create_is_domain_project_not_allowed(self): - """Tests that we can't create more than one project acting as domain. - - This method will be used at any test that require the creation of a - project that act as a domain. LDAP does not support multiple domains - and the only domain it has (default) is immutable. - """ - domain = self._get_domain_fixture() - project = {'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex, - 'description': '', - 'domain_id': domain['id'], - 'enabled': True, - 'parent_id': None, - 'is_domain': True} - - self.assertRaises(exception.ValidationError, - self.resource_api.create_project, - project['id'], project) - def test_update_is_domain_field(self): domain = self._get_domain_fixture() - project = {'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex, - 'description': '', - 'domain_id': domain['id'], - 'enabled': True, - 'parent_id': None, - 'is_domain': False} - self.resource_api.create_project(project['id'], project) + project = unit.new_project_ref(domain_id=domain['id']) + project = self.resource_api.create_project(project['id'], project) # Try to update the is_domain field to True project['is_domain'] = True @@ -1772,97 +1618,87 @@ class LDAPIdentity(BaseLDAPIdentity, unit.TestCase): project['id'], project) def test_delete_is_domain_project(self): - self._assert_create_is_domain_project_not_allowed() + self.skipTest('Resource LDAP has been removed') def test_create_domain_under_regular_project_hierarchy_fails(self): - self._assert_create_hierarchy_not_allowed() + self.skipTest('Resource LDAP has been removed') def test_create_not_is_domain_project_under_is_domain_hierarchy(self): - self._assert_create_hierarchy_not_allowed() + self.skipTest('Resource LDAP has been removed') - def test_create_is_domain_project(self): - self._assert_create_is_domain_project_not_allowed() + def test_create_project_passing_is_domain_flag_true(self): + self.skipTest('Resource LDAP has been removed') def test_create_project_with_parent_id_and_without_domain_id(self): - self._assert_create_hierarchy_not_allowed() + self.skipTest('Resource LDAP has been removed') def test_check_leaf_projects(self): - projects = self._assert_create_hierarchy_not_allowed() - for project in projects: - self.assertTrue(self.resource_api.is_leaf_project(project)) + self.skipTest('Resource LDAP has been removed') def test_list_projects_in_subtree(self): - projects = self._assert_create_hierarchy_not_allowed() - for project in projects: - subtree_list = self.resource_api.list_projects_in_subtree( - project['id']) - self.assertEqual(0, len(subtree_list)) + self.skipTest('Resource LDAP has been removed') def test_list_projects_in_subtree_with_circular_reference(self): - self._assert_create_hierarchy_not_allowed() + self.skipTest('Resource LDAP has been removed') def test_list_project_parents(self): - projects = self._assert_create_hierarchy_not_allowed() - for project in projects: - parents_list = self.resource_api.list_project_parents( - project['id']) - self.assertEqual(0, len(parents_list)) + self.skipTest('Resource LDAP has been removed') + + def test_update_project_enabled_cascade(self): + self.skipTest('Resource LDAP has been removed') + + def test_cannot_enable_cascade_with_parent_disabled(self): + self.skipTest('Resource LDAP has been removed') def test_hierarchical_projects_crud(self): - self._assert_create_hierarchy_not_allowed() + self.skipTest('Resource LDAP has been removed') def test_create_project_under_disabled_one(self): - self._assert_create_hierarchy_not_allowed() + self.skipTest('Resource LDAP has been removed') def test_create_project_with_invalid_parent(self): - self._assert_create_hierarchy_not_allowed() + self.skipTest('Resource LDAP has been removed') def test_create_leaf_project_with_invalid_domain(self): - self._assert_create_hierarchy_not_allowed() + self.skipTest('Resource LDAP has been removed') def test_update_project_parent(self): - self._assert_create_hierarchy_not_allowed() + self.skipTest('Resource LDAP has been removed') def test_enable_project_with_disabled_parent(self): - self._assert_create_hierarchy_not_allowed() + self.skipTest('Resource LDAP has been removed') def test_disable_hierarchical_leaf_project(self): - self._assert_create_hierarchy_not_allowed() + self.skipTest('Resource LDAP has been removed') def test_disable_hierarchical_not_leaf_project(self): - self._assert_create_hierarchy_not_allowed() + self.skipTest('Resource LDAP has been removed') def test_delete_hierarchical_leaf_project(self): - self._assert_create_hierarchy_not_allowed() + self.skipTest('Resource LDAP has been removed') def test_delete_hierarchical_not_leaf_project(self): - self._assert_create_hierarchy_not_allowed() + self.skipTest('Resource LDAP has been removed') def test_check_hierarchy_depth(self): - projects = self._assert_create_hierarchy_not_allowed() - for project in projects: - depth = self._get_hierarchy_depth(project['id']) - self.assertEqual(1, depth) + self.skipTest('Resource LDAP has been removed') def test_multi_role_grant_by_user_group_on_project_domain(self): # This is a partial implementation of the standard test that - # is defined in test_backend.py. It omits both domain and - # group grants. since neither of these are yet supported by - # the ldap backend. + # is defined in unit.assignment.test_backends.py. It omits + # both domain and group grants. since neither of these are + # yet supported by the ldap backend. role_list = [] for _ in range(2): - role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + role = unit.new_role_ref() self.role_api.create_role(role['id'], role) role_list.append(role) - user1 = {'name': uuid.uuid4().hex, - 'domain_id': CONF.identity.default_domain_id, - 'password': uuid.uuid4().hex, - 'enabled': True} + user1 = self.new_user_ref(domain_id=CONF.identity.default_domain_id) user1 = self.identity_api.create_user(user1) - project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, - 'domain_id': CONF.identity.default_domain_id} + project1 = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) self.resource_api.create_project(project1['id'], project1) self.assignment_api.add_role_to_user_and_project( @@ -1947,7 +1783,7 @@ class LDAPIdentity(BaseLDAPIdentity, unit.TestCase): expected_group_ids = [] numgroups = 3 for _ in range(numgroups): - group = {'name': uuid.uuid4().hex, 'domain_id': domain['id']} + group = unit.new_group_ref(domain_id=domain['id']) group = self.identity_api.create_group(group) expected_group_ids.append(group['id']) # Fetch the test groups and ensure that they don't contain a dn. @@ -1960,16 +1796,14 @@ class LDAPIdentity(BaseLDAPIdentity, unit.TestCase): def test_list_groups_for_user_no_dn(self): # Create a test user. - user = {'name': uuid.uuid4().hex, - 'domain_id': CONF.identity.default_domain_id, - 'password': uuid.uuid4().hex, 'enabled': True} + user = self.new_user_ref(domain_id=CONF.identity.default_domain_id) user = self.identity_api.create_user(user) # Create some test groups and add the test user as a member. domain = self._get_domain_fixture() expected_group_ids = [] numgroups = 3 for _ in range(numgroups): - group = {'name': uuid.uuid4().hex, 'domain_id': domain['id']} + group = unit.new_group_ref(domain_id=domain['id']) group = self.identity_api.create_group(group) expected_group_ids.append(group['id']) self.identity_api.add_user_to_group(user['id'], group['id']) @@ -1987,9 +1821,7 @@ class LDAPIdentity(BaseLDAPIdentity, unit.TestCase): CONF.identity.default_domain_id) driver.user.id_attr = 'mail' - user = {'name': u'fäké1', - 'password': u'fäképass1', - 'domain_id': CONF.identity.default_domain_id} + user = self.new_user_ref(domain_id=CONF.identity.default_domain_id) user = self.identity_api.create_user(user) user_ref = self.identity_api.get_user(user['id']) # 'email' attribute should've created because it is also being used @@ -2083,6 +1915,35 @@ class LDAPIdentity(BaseLDAPIdentity, unit.TestCase): self.assertEqual('Foo Bar', user_ref['name']) +class LDAPLimitTests(unit.TestCase, identity_tests.LimitTests): + def setUp(self): + super(LDAPLimitTests, self).setUp() + + self.useFixture(ldapdb.LDAPDatabase()) + self.useFixture(database.Database(self.sql_driver_version_overrides)) + self.load_backends() + self.load_fixtures(default_fixtures) + identity_tests.LimitTests.setUp(self) + _assert_backends(self, + assignment='sql', + identity='ldap', + resource='sql') + + def config_overrides(self): + super(LDAPLimitTests, self).config_overrides() + self.config_fixture.config(group='identity', driver='ldap') + self.config_fixture.config(group='identity', + list_limit=len(default_fixtures.USERS) - 1) + + def config_files(self): + config_files = super(LDAPLimitTests, self).config_files() + config_files.append(unit.dirs.tests_conf('backend_ldap.conf')) + return config_files + + def test_list_projects_filtered_and_limited(self): + self.skipTest("ldap for storing projects is deprecated") + + class LDAPIdentityEnabledEmulation(LDAPIdentity): def setUp(self): super(LDAPIdentityEnabledEmulation, self).setUp() @@ -2092,10 +1953,7 @@ class LDAPIdentityEnabledEmulation(LDAPIdentity): for obj in [self.tenant_bar, self.tenant_baz, self.user_foo, self.user_two, self.user_badguy]: obj.setdefault('enabled', True) - _assert_backends(self, - assignment='ldap', - identity='ldap', - resource='ldap') + _assert_backends(self, identity='ldap') def load_fixtures(self, fixtures): # Override super impl since need to create group container. @@ -2110,60 +1968,62 @@ class LDAPIdentityEnabledEmulation(LDAPIdentity): def config_overrides(self): super(LDAPIdentityEnabledEmulation, self).config_overrides() self.config_fixture.config(group='ldap', - user_enabled_emulation=True, - project_enabled_emulation=True) + user_enabled_emulation=True) def test_project_crud(self): # NOTE(topol): LDAPIdentityEnabledEmulation will create an # enabled key in the project dictionary so this # method override handles this side-effect - project = { - 'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex, - 'domain_id': CONF.identity.default_domain_id, - 'description': uuid.uuid4().hex, - 'parent_id': None, - 'is_domain': False} - - self.resource_api.create_project(project['id'], project) + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + + project = self.resource_api.create_project(project['id'], project) project_ref = self.resource_api.get_project(project['id']) # self.resource_api.create_project adds an enabled # key with a value of True when LDAPIdentityEnabledEmulation # is used so we now add this expected key to the project dictionary project['enabled'] = True - self.assertDictEqual(project_ref, project) + self.assertDictEqual(project, project_ref) project['description'] = uuid.uuid4().hex self.resource_api.update_project(project['id'], project) project_ref = self.resource_api.get_project(project['id']) - self.assertDictEqual(project_ref, project) + self.assertDictEqual(project, project_ref) self.resource_api.delete_project(project['id']) self.assertRaises(exception.ProjectNotFound, self.resource_api.get_project, project['id']) - def test_user_crud(self): - user_dict = { - 'domain_id': CONF.identity.default_domain_id, - 'name': uuid.uuid4().hex, - 'password': uuid.uuid4().hex} + @mock.patch.object(versionutils, 'report_deprecated_feature') + def test_user_crud(self, mock_deprecator): + # NOTE(stevemar): As of the Mitaka release, we now check for calls that + # the LDAP write functionality has been deprecated. + user_dict = self.new_user_ref( + domain_id=CONF.identity.default_domain_id) user = self.identity_api.create_user(user_dict) - user_dict['enabled'] = True - user_ref = self.identity_api.get_user(user['id']) + args, _kwargs = mock_deprecator.call_args + self.assertIn("create_user for the LDAP identity backend", args[1]) + del user_dict['password'] + user_ref = self.identity_api.get_user(user['id']) user_ref_dict = {x: user_ref[x] for x in user_ref} self.assertDictContainsSubset(user_dict, user_ref_dict) user_dict['password'] = uuid.uuid4().hex - self.identity_api.update_user(user['id'], user) - user_ref = self.identity_api.get_user(user['id']) + self.identity_api.update_user(user['id'], user_dict) + args, _kwargs = mock_deprecator.call_args + self.assertIn("update_user for the LDAP identity backend", args[1]) + del user_dict['password'] + user_ref = self.identity_api.get_user(user['id']) user_ref_dict = {x: user_ref[x] for x in user_ref} self.assertDictContainsSubset(user_dict, user_ref_dict) self.identity_api.delete_user(user['id']) + args, _kwargs = mock_deprecator.call_args + self.assertIn("delete_user for the LDAP identity backend", args[1]) self.assertRaises(exception.UserNotFound, self.identity_api.get_user, user['id']) @@ -2192,8 +2052,8 @@ class LDAPIdentityEnabledEmulation(LDAPIdentity): self.load_fixtures(default_fixtures) # Create a user and ensure they are enabled. - user1 = {'name': u'fäké1', 'enabled': True, - 'domain_id': CONF.identity.default_domain_id} + user1 = unit.new_user_ref(enabled=True, + domain_id=CONF.identity.default_domain_id) user_ref = self.identity_api.create_user(user1) self.assertIs(True, user_ref['enabled']) @@ -2208,14 +2068,12 @@ class LDAPIdentityEnabledEmulation(LDAPIdentity): self.load_backends() self.load_fixtures(default_fixtures) - user1 = {'name': u'fäké1', 'enabled': True, - 'domain_id': CONF.identity.default_domain_id} + user1 = self.new_user_ref(domain_id=CONF.identity.default_domain_id) - user2 = {'name': u'fäké2', 'enabled': False, - 'domain_id': CONF.identity.default_domain_id} + user2 = self.new_user_ref(enabled=False, + domain_id=CONF.identity.default_domain_id) - user3 = {'name': u'fäké3', - 'domain_id': CONF.identity.default_domain_id} + user3 = self.new_user_ref(domain_id=CONF.identity.default_domain_id) # Ensure that the enabled LDAP attribute is not set for a # newly created enabled user. @@ -2282,121 +2140,103 @@ class LDAPIdentityEnabledEmulation(LDAPIdentity): user_ref = user_api.get('123456789') self.assertIs(False, user_ref['enabled']) + def test_escape_member_dn(self): + # The enabled member DN is properly escaped when querying for enabled + # user. -class LdapIdentitySqlAssignment(BaseLDAPIdentity, unit.SQLDriverOverrides, - unit.TestCase): + object_id = uuid.uuid4().hex + driver = self.identity_api._select_identity_driver( + CONF.identity.default_domain_id) - def config_files(self): - config_files = super(LdapIdentitySqlAssignment, self).config_files() - config_files.append(unit.dirs.tests_conf('backend_ldap_sql.conf')) - return config_files + # driver.user is the EnabledEmuMixIn implementation used for this test. + mixin_impl = driver.user - def setUp(self): - sqldb = self.useFixture(database.Database()) - super(LdapIdentitySqlAssignment, self).setUp() - self.ldapdb.clear() - self.load_backends() - cache.configure_cache_region(cache.REGION) + # ) is a special char in a filter and must be escaped. + sample_dn = 'cn=foo)bar' + # LDAP requires ) is escaped by being replaced with "\29" + sample_dn_filter_esc = r'cn=foo\29bar' - sqldb.recreate() - self.load_fixtures(default_fixtures) - # defaulted by the data load - self.user_foo['enabled'] = True - _assert_backends(self, - assignment='sql', - identity='ldap', - resource='sql') + # Override the tree_dn, it's used to build the enabled member filter + mixin_impl.tree_dn = sample_dn - def config_overrides(self): - super(LdapIdentitySqlAssignment, self).config_overrides() - self.config_fixture.config(group='identity', driver='ldap') - self.config_fixture.config(group='resource', driver='sql') - self.config_fixture.config(group='assignment', driver='sql') + # The filter that _get_enabled is going to build contains the + # tree_dn, which better be escaped in this case. + exp_filter = '(%s=%s=%s,%s)' % ( + mixin_impl.member_attribute, mixin_impl.id_attr, object_id, + sample_dn_filter_esc) - def test_domain_crud(self): - pass + with mixin_impl.get_connection() as conn: + m = self.useFixture(mockpatch.PatchObject(conn, 'search_s')).mock + mixin_impl._get_enabled(object_id, conn) + # The 3rd argument is the DN. + self.assertEqual(exp_filter, m.call_args[0][2]) - def test_list_domains(self): - domains = self.resource_api.list_domains() - self.assertEqual([resource.calc_default_domain()], domains) - def test_list_domains_non_default_domain_id(self): - # If change the default_domain_id, the ID of the default domain - # returned by list_domains doesn't change because the SQL identity - # backend reads it from the database, which doesn't get updated by - # config change. +class LDAPPosixGroupsTest(unit.TestCase): - orig_default_domain_id = CONF.identity.default_domain_id + def setUp(self): - new_domain_id = uuid.uuid4().hex - self.config_fixture.config(group='identity', - default_domain_id=new_domain_id) + super(LDAPPosixGroupsTest, self).setUp() - domains = self.resource_api.list_domains() + self.useFixture(ldapdb.LDAPDatabase()) + self.useFixture(database.Database()) - self.assertEqual(orig_default_domain_id, domains[0]['id']) + self.load_backends() + self.load_fixtures(default_fixtures) - def test_create_domain(self): - domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, - 'enabled': True} - self.assertRaises(exception.Forbidden, - self.resource_api.create_domain, - domain['id'], - domain) + _assert_backends(self, identity='ldap') - def test_get_and_remove_role_grant_by_group_and_domain(self): - # TODO(henry-nash): We should really rewrite the tests in test_backend - # to be more flexible as to where the domains are sourced from, so - # that we would not need to override such tests here. This is raised - # as bug 1373865. - new_domain = self._get_domain_fixture() - new_group = {'domain_id': new_domain['id'], 'name': uuid.uuid4().hex} - new_group = self.identity_api.create_group(new_group) - new_user = {'name': 'new_user', 'password': uuid.uuid4().hex, - 'enabled': True, 'domain_id': new_domain['id']} - new_user = self.identity_api.create_user(new_user) - self.identity_api.add_user_to_group(new_user['id'], - new_group['id']) + def load_fixtures(self, fixtures): + # Override super impl since need to create group container. + create_group_container(self.identity_api) + super(LDAPPosixGroupsTest, self).load_fixtures(fixtures) - roles_ref = self.assignment_api.list_grants( - group_id=new_group['id'], - domain_id=new_domain['id']) - self.assertEqual(0, len(roles_ref)) + def config_overrides(self): + super(LDAPPosixGroupsTest, self).config_overrides() + self.config_fixture.config(group='identity', driver='ldap') + self.config_fixture.config(group='ldap', group_members_are_ids=True, + group_member_attribute='memberUID') - self.assignment_api.create_grant(group_id=new_group['id'], - domain_id=new_domain['id'], - role_id='member') + def config_files(self): + config_files = super(LDAPPosixGroupsTest, self).config_files() + config_files.append(unit.dirs.tests_conf('backend_ldap.conf')) + return config_files - roles_ref = self.assignment_api.list_grants( - group_id=new_group['id'], - domain_id=new_domain['id']) - self.assertDictEqual(roles_ref[0], self.role_member) + def _get_domain_fixture(self): + """Domains in LDAP are read-only, so just return the static one.""" + return self.resource_api.get_domain(CONF.identity.default_domain_id) - self.assignment_api.delete_grant(group_id=new_group['id'], - domain_id=new_domain['id'], - role_id='member') - roles_ref = self.assignment_api.list_grants( - group_id=new_group['id'], - domain_id=new_domain['id']) - self.assertEqual(0, len(roles_ref)) - self.assertRaises(exception.NotFound, - self.assignment_api.delete_grant, - group_id=new_group['id'], - domain_id=new_domain['id'], - role_id='member') + def test_posix_member_id(self): + domain = self._get_domain_fixture() + new_group = unit.new_group_ref(domain_id=domain['id']) + new_group = self.identity_api.create_group(new_group) + # Make sure we get an empty list back on a new group, not an error. + user_refs = self.identity_api.list_users_in_group(new_group['id']) + self.assertEqual([], user_refs) + # Make sure we get the correct users back once they have been added + # to the group. + new_user = unit.new_user_ref(domain_id=domain['id']) + new_user = self.identity_api.create_user(new_user) - def test_project_enabled_ignored_disable_error(self): - # Override - self.skipTest("Doesn't apply since LDAP configuration is ignored for " - "SQL assignment backend.") + # NOTE(amakarov): Create the group directly using LDAP operations + # rather than going through the manager. + group_api = self.identity_api.driver.group + group_ref = group_api.get(new_group['id']) + mod = (ldap.MOD_ADD, group_api.member_attribute, new_user['id']) + conn = group_api.get_connection() + conn.modify_s(group_ref['dn'], [mod]) - def test_list_role_assignments_filtered_by_role(self): - # Domain roles are supported by the SQL Assignment backend - base = super(BaseLDAPIdentity, self) - base.test_list_role_assignments_filtered_by_role() + # Testing the case "the group contains a user" + user_refs = self.identity_api.list_users_in_group(new_group['id']) + self.assertIn(new_user['id'], (x['id'] for x in user_refs)) + # Testing the case "the user is a member of a group" + group_refs = self.identity_api.list_groups_for_user(new_user['id']) + self.assertIn(new_group['id'], (x['id'] for x in group_refs)) -class LdapIdentitySqlAssignmentWithMapping(LdapIdentitySqlAssignment): + +class LdapIdentityWithMapping( + BaseLDAPIdentity, unit.SQLDriverOverrides, unit.TestCase): """Class to test mapping of default LDAP backend. The default configuration is not to enable mapping when using a single @@ -2405,8 +2245,28 @@ class LdapIdentitySqlAssignmentWithMapping(LdapIdentitySqlAssignment): Setting backward_compatible_ids to False will enable this mapping. """ + + def config_files(self): + config_files = super(LdapIdentityWithMapping, self).config_files() + config_files.append(unit.dirs.tests_conf('backend_ldap_sql.conf')) + return config_files + + def setUp(self): + sqldb = self.useFixture(database.Database()) + super(LdapIdentityWithMapping, self).setUp() + self.ldapdb.clear() + self.load_backends() + cache.configure_cache() + + sqldb.recreate() + self.load_fixtures(default_fixtures) + # defaulted by the data load + self.user_foo['enabled'] = True + _assert_backends(self, identity='ldap') + def config_overrides(self): - super(LdapIdentitySqlAssignmentWithMapping, self).config_overrides() + super(LdapIdentityWithMapping, self).config_overrides() + self.config_fixture.config(group='identity', driver='ldap') self.config_fixture.config(group='identity_mapping', backward_compatible_ids=False) @@ -2420,13 +2280,9 @@ class LdapIdentitySqlAssignmentWithMapping(LdapIdentitySqlAssignment): """ initial_mappings = len(mapping_sql.list_id_mappings()) - user1 = {'name': uuid.uuid4().hex, - 'domain_id': CONF.identity.default_domain_id, - 'password': uuid.uuid4().hex, 'enabled': True} + user1 = self.new_user_ref(domain_id=CONF.identity.default_domain_id) user1 = self.identity_api.create_user(user1) - user2 = {'name': uuid.uuid4().hex, - 'domain_id': CONF.identity.default_domain_id, - 'password': uuid.uuid4().hex, 'enabled': True} + user2 = self.new_user_ref(domain_id=CONF.identity.default_domain_id) user2 = self.identity_api.create_user(user2) mappings = mapping_sql.list_id_mappings() self.assertEqual(initial_mappings + 2, len(mappings)) @@ -2453,35 +2309,29 @@ class LdapIdentitySqlAssignmentWithMapping(LdapIdentitySqlAssignment): self.skipTest('N/A: We never generate the same ID for a user and ' 'group in our mapping table') + def test_list_domains(self): + domains = self.resource_api.list_domains() + self.assertEqual([resource.calc_default_domain()], domains) + class BaseMultiLDAPandSQLIdentity(object): """Mixin class with support methods for domain-specific config testing.""" - def create_user(self, domain_id): - user = {'name': uuid.uuid4().hex, - 'domain_id': domain_id, - 'password': uuid.uuid4().hex, - 'enabled': True} - user_ref = self.identity_api.create_user(user) - # Put the password back in, since this is used later by tests to - # authenticate. - user_ref['password'] = user['password'] - return user_ref - def create_users_across_domains(self): """Create a set of users, each with a role on their own domain.""" - # We also will check that the right number of id mappings get created initial_mappings = len(mapping_sql.list_id_mappings()) - self.users['user0'] = self.create_user( + self.users['user0'] = unit.create_user( + self.identity_api, self.domains['domain_default']['id']) self.assignment_api.create_grant( user_id=self.users['user0']['id'], domain_id=self.domains['domain_default']['id'], role_id=self.role_member['id']) for x in range(1, self.domain_count): - self.users['user%s' % x] = self.create_user( + self.users['user%s' % x] = unit.create_user( + self.identity_api, self.domains['domain%s' % x]['id']) self.assignment_api.create_grant( user_id=self.users['user%s' % x]['id'], @@ -2506,13 +2356,13 @@ class BaseMultiLDAPandSQLIdentity(object): self.identity_api._get_domain_driver_and_entity_id( user['id'])) - if expected_status == 200: + if expected_status == http_client.OK: ref = driver.get_user(entity_id) ref = self.identity_api._set_domain_id_and_mapping( ref, domain_id, driver, map.EntityType.USER) user = user.copy() del user['password'] - self.assertDictEqual(ref, user) + self.assertDictEqual(user, ref) else: # TODO(henry-nash): Use AssertRaises here, although # there appears to be an issue with using driver.get_user @@ -2570,6 +2420,7 @@ class MultiLDAPandSQLIdentity(BaseLDAPIdentity, unit.SQLDriverOverrides, domain. """ + def setUp(self): sqldb = self.useFixture(database.Database()) super(MultiLDAPandSQLIdentity, self).setUp() @@ -2614,11 +2465,14 @@ class MultiLDAPandSQLIdentity(BaseLDAPIdentity, unit.SQLDriverOverrides, # Create some identity entities BEFORE we switch to multi-backend, so # we can test that these are still accessible self.users = {} - self.users['userA'] = self.create_user( + self.users['userA'] = unit.create_user( + self.identity_api, self.domains['domain_default']['id']) - self.users['userB'] = self.create_user( + self.users['userB'] = unit.create_user( + self.identity_api, self.domains['domain1']['id']) - self.users['userC'] = self.create_user( + self.users['userC'] = unit.create_user( + self.identity_api, self.domains['domain3']['id']) def enable_multi_domain(self): @@ -2631,7 +2485,8 @@ class MultiLDAPandSQLIdentity(BaseLDAPIdentity, unit.SQLDriverOverrides, """ self.config_fixture.config( group='identity', domain_specific_drivers_enabled=True, - domain_config_dir=unit.TESTCONF + '/domain_configs_multi_ldap') + domain_config_dir=unit.TESTCONF + '/domain_configs_multi_ldap', + list_limit=1000) self.config_fixture.config(group='identity_mapping', backward_compatible_ids=False) @@ -2640,14 +2495,6 @@ class MultiLDAPandSQLIdentity(BaseLDAPIdentity, unit.SQLDriverOverrides, # if no specific config defined for this domain return self.identity_api.domain_configs.get_domain_conf(domain_id) - def test_list_domains(self): - self.skipTest( - 'N/A: Not relevant for multi ldap testing') - - def test_list_domains_non_default_domain_id(self): - self.skipTest( - 'N/A: Not relevant for multi ldap testing') - def test_list_users(self): # Override the standard list users, since we have added an extra user # to the default domain, so the number of expected users is one more @@ -2664,6 +2511,36 @@ class MultiLDAPandSQLIdentity(BaseLDAPIdentity, unit.SQLDriverOverrides, self.assertNotIn('password', user_ref) self.assertEqual(expected_user_ids, user_ids) + @mock.patch.object(common_ldap_core.BaseLdap, '_ldap_get_all') + def test_list_limit_domain_specific_inheritance(self, ldap_get_all): + # passiging hints is important, because if it's not passed, limiting + # is considered be disabled + hints = driver_hints.Hints() + self.identity_api.list_users( + domain_scope=self.domains['domain2']['id'], + hints=hints) + # since list_limit is not specified in keystone.domain2.conf, it should + # take the default, which is 1000 + self.assertTrue(ldap_get_all.called) + args, kwargs = ldap_get_all.call_args + hints = args[0] + self.assertEqual(1000, hints.limit['limit']) + + @mock.patch.object(common_ldap_core.BaseLdap, '_ldap_get_all') + def test_list_limit_domain_specific_override(self, ldap_get_all): + # passiging hints is important, because if it's not passed, limiting + # is considered to be disabled + hints = driver_hints.Hints() + self.identity_api.list_users( + domain_scope=self.domains['domain1']['id'], + hints=hints) + # this should have the list_limit set in Keystone.domain1.conf, which + # is 101 + self.assertTrue(ldap_get_all.called) + args, kwargs = ldap_get_all.call_args + hints = args[0] + self.assertEqual(101, hints.limit['limit']) + def test_domain_segregation(self): """Test that separate configs have segregated the domain. @@ -2680,21 +2557,23 @@ class MultiLDAPandSQLIdentity(BaseLDAPIdentity, unit.SQLDriverOverrides, check_user = self.check_user check_user(self.users['user0'], - self.domains['domain_default']['id'], 200) + self.domains['domain_default']['id'], http_client.OK) for domain in [self.domains['domain1']['id'], self.domains['domain2']['id'], self.domains['domain3']['id'], self.domains['domain4']['id']]: check_user(self.users['user0'], domain, exception.UserNotFound) - check_user(self.users['user1'], self.domains['domain1']['id'], 200) + check_user(self.users['user1'], self.domains['domain1']['id'], + http_client.OK) for domain in [self.domains['domain_default']['id'], self.domains['domain2']['id'], self.domains['domain3']['id'], self.domains['domain4']['id']]: check_user(self.users['user1'], domain, exception.UserNotFound) - check_user(self.users['user2'], self.domains['domain2']['id'], 200) + check_user(self.users['user2'], self.domains['domain2']['id'], + http_client.OK) for domain in [self.domains['domain_default']['id'], self.domains['domain1']['id'], self.domains['domain3']['id'], @@ -2704,10 +2583,14 @@ class MultiLDAPandSQLIdentity(BaseLDAPIdentity, unit.SQLDriverOverrides, # domain3 and domain4 share the same backend, so you should be # able to see user3 and user4 from either. - check_user(self.users['user3'], self.domains['domain3']['id'], 200) - check_user(self.users['user3'], self.domains['domain4']['id'], 200) - check_user(self.users['user4'], self.domains['domain3']['id'], 200) - check_user(self.users['user4'], self.domains['domain4']['id'], 200) + check_user(self.users['user3'], self.domains['domain3']['id'], + http_client.OK) + check_user(self.users['user3'], self.domains['domain4']['id'], + http_client.OK) + check_user(self.users['user4'], self.domains['domain3']['id'], + http_client.OK) + check_user(self.users['user4'], self.domains['domain4']['id'], + http_client.OK) for domain in [self.domains['domain_default']['id'], self.domains['domain1']['id'], @@ -2789,19 +2672,12 @@ class MultiLDAPandSQLIdentity(BaseLDAPIdentity, unit.SQLDriverOverrides, self.assertEqual('fake://memory1', conf.ldap.url) def test_delete_domain_with_user_added(self): - domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, - 'enabled': True} - project = {'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex, - 'domain_id': domain['id'], - 'description': uuid.uuid4().hex, - 'parent_id': None, - 'enabled': True, - 'is_domain': False} + domain = unit.new_domain_ref() + project = unit.new_project_ref(domain_id=domain['id']) self.resource_api.create_domain(domain['id'], domain) - self.resource_api.create_project(project['id'], project) + project = self.resource_api.create_project(project['id'], project) project_ref = self.resource_api.get_project(project['id']) - self.assertDictEqual(project_ref, project) + self.assertDictEqual(project, project_ref) self.assignment_api.create_grant(user_id=self.user_foo['id'], project_id=project['id'], @@ -2839,13 +2715,37 @@ class MultiLDAPandSQLIdentity(BaseLDAPIdentity, unit.SQLDriverOverrides, def test_list_role_assignment_by_domain(self): # With multi LDAP this method should work, so override the override # from BaseLDAPIdentity - super(BaseLDAPIdentity, self).test_list_role_assignment_by_domain + super(BaseLDAPIdentity, self).test_list_role_assignment_by_domain() def test_list_role_assignment_by_user_with_domain_group_roles(self): # With multi LDAP this method should work, so override the override # from BaseLDAPIdentity super(BaseLDAPIdentity, self).\ - test_list_role_assignment_by_user_with_domain_group_roles + test_list_role_assignment_by_user_with_domain_group_roles() + + def test_list_role_assignment_using_sourced_groups_with_domains(self): + # With SQL Assignment this method should work, so override the override + # from BaseLDAPIdentity + base = super(BaseLDAPIdentity, self) + base.test_list_role_assignment_using_sourced_groups_with_domains() + + def test_create_project_with_domain_id_and_without_parent_id(self): + # With multi LDAP this method should work, so override the override + # from BaseLDAPIdentity + super(BaseLDAPIdentity, self).\ + test_create_project_with_domain_id_and_without_parent_id() + + def test_create_project_with_domain_id_mismatch_to_parent_domain(self): + # With multi LDAP this method should work, so override the override + # from BaseLDAPIdentity + super(BaseLDAPIdentity, self).\ + test_create_project_with_domain_id_mismatch_to_parent_domain() + + def test_remove_foreign_assignments_when_deleting_a_domain(self): + # With multi LDAP this method should work, so override the override + # from BaseLDAPIdentity + base = super(BaseLDAPIdentity, self) + base.test_remove_foreign_assignments_when_deleting_a_domain() class MultiLDAPandSQLIdentityDomainConfigsInSQL(MultiLDAPandSQLIdentity): @@ -2870,7 +2770,7 @@ class MultiLDAPandSQLIdentityDomainConfigsInSQL(MultiLDAPandSQLIdentity): def enable_multi_domain(self): # The values below are the same as in the domain_configs_multi_ldap - # cdirectory of test config_files. + # directory of test config_files. default_config = { 'ldap': {'url': 'fake://memory', 'user': 'cn=Admin', @@ -2883,7 +2783,8 @@ class MultiLDAPandSQLIdentityDomainConfigsInSQL(MultiLDAPandSQLIdentity): 'user': 'cn=Admin', 'password': 'password', 'suffix': 'cn=example,cn=com'}, - 'identity': {'driver': 'ldap'} + 'identity': {'driver': 'ldap', + 'list_limit': '101'} } domain2_config = { 'ldap': {'url': 'fake://memory', @@ -2904,7 +2805,8 @@ class MultiLDAPandSQLIdentityDomainConfigsInSQL(MultiLDAPandSQLIdentity): self.config_fixture.config( group='identity', domain_specific_drivers_enabled=True, - domain_configurations_from_database=True) + domain_configurations_from_database=True, + list_limit=1000) self.config_fixture.config(group='identity_mapping', backward_compatible_ids=False) @@ -2933,7 +2835,6 @@ class MultiLDAPandSQLIdentityDomainConfigsInSQL(MultiLDAPandSQLIdentity): def test_reloading_domain_config(self): """Ensure domain drivers are reloaded on a config modification.""" - domain_cfgs = self.identity_api.domain_configs # Create a new config for the default domain, hence overwriting the @@ -2965,7 +2866,6 @@ class MultiLDAPandSQLIdentityDomainConfigsInSQL(MultiLDAPandSQLIdentity): def test_setting_multiple_sql_driver_raises_exception(self): """Ensure setting multiple domain specific sql drivers is prevented.""" - new_config = {'identity': {'driver': 'sql'}} self.domain_config_api.create_config( CONF.identity.default_domain_id, new_config) @@ -2979,7 +2879,6 @@ class MultiLDAPandSQLIdentityDomainConfigsInSQL(MultiLDAPandSQLIdentity): def test_same_domain_gets_sql_driver(self): """Ensure we can set an SQL driver if we have had it before.""" - new_config = {'identity': {'driver': 'sql'}} self.domain_config_api.create_config( CONF.identity.default_domain_id, new_config) @@ -2997,8 +2896,7 @@ class MultiLDAPandSQLIdentityDomainConfigsInSQL(MultiLDAPandSQLIdentity): def test_delete_domain_clears_sql_registration(self): """Ensure registration is deleted when a domain is deleted.""" - - domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + domain = unit.new_domain_ref() domain = self.resource_api.create_domain(domain['id'], domain) new_config = {'identity': {'driver': 'sql'}} self.domain_config_api.create_config(domain['id'], new_config) @@ -3025,8 +2923,7 @@ class MultiLDAPandSQLIdentityDomainConfigsInSQL(MultiLDAPandSQLIdentity): def test_orphaned_registration_does_not_prevent_getting_sql_driver(self): """Ensure we self heal an orphaned sql registration.""" - - domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + domain = unit.new_domain_ref() domain = self.resource_api.create_domain(domain['id'], domain) new_config = {'identity': {'driver': 'sql'}} self.domain_config_api.create_config(domain['id'], new_config) @@ -3047,7 +2944,7 @@ class MultiLDAPandSQLIdentityDomainConfigsInSQL(MultiLDAPandSQLIdentity): # should still be able to set another domain to SQL, since we should # self heal this issue. - self.resource_api.driver.delete_domain(domain['id']) + self.resource_api.driver.delete_project(domain['id']) # Invalidate cache (so we will see the domain has gone) self.resource_api.get_domain.invalidate( self.resource_api, domain['id']) @@ -3072,6 +2969,7 @@ class DomainSpecificLDAPandSQLIdentity( Although the default driver still exists, we don't use it. """ + def setUp(self): sqldb = self.useFixture(database.Database()) super(DomainSpecificLDAPandSQLIdentity, self).setUp() @@ -3133,6 +3031,17 @@ class DomainSpecificLDAPandSQLIdentity( self.skipTest( 'N/A: Not relevant for multi ldap testing') + def test_not_delete_domain_with_enabled_subdomains(self): + self.skipTest( + 'N/A: Not relevant for multi ldap testing') + + def test_delete_domain(self): + # With this restricted multi LDAP class, tests that use multiple + # domains and identity, are still not supported + self.assertRaises( + exception.DomainNotFound, + super(BaseLDAPIdentity, self).test_delete_domain_with_project_api) + def test_list_users(self): # Override the standard list users, since we have added an extra user # to the default domain, so the number of expected users is one more @@ -3164,12 +3073,12 @@ class DomainSpecificLDAPandSQLIdentity( # driver, but won't find it via any other domain driver self.check_user(self.users['user0'], - self.domains['domain_default']['id'], 200) + self.domains['domain_default']['id'], http_client.OK) self.check_user(self.users['user0'], self.domains['domain1']['id'], exception.UserNotFound) self.check_user(self.users['user1'], - self.domains['domain1']['id'], 200) + self.domains['domain1']['id'], http_client.OK) self.check_user(self.users['user1'], self.domains['domain_default']['id'], exception.UserNotFound) @@ -3182,10 +3091,10 @@ class DomainSpecificLDAPandSQLIdentity( domain_scope=self.domains['domain1']['id']), matchers.HasLength(1)) - def test_add_role_grant_to_user_and_project_404(self): + def test_add_role_grant_to_user_and_project_returns_not_found(self): self.skipTest('Blocked by bug 1101287') - def test_get_role_grants_for_user_and_project_404(self): + def test_get_role_grants_for_user_and_project_returns_not_found(self): self.skipTest('Blocked by bug 1101287') def test_list_projects_for_user_with_grants(self): @@ -3223,6 +3132,25 @@ class DomainSpecificLDAPandSQLIdentity( base = super(BaseLDAPIdentity, self) base.test_list_role_assignments_filtered_by_role() + def test_delete_domain_with_project_api(self): + # With this restricted multi LDAP class, tests that use multiple + # domains and identity, are still not supported + self.assertRaises( + exception.DomainNotFound, + super(BaseLDAPIdentity, self).test_delete_domain_with_project_api) + + def test_create_project_with_domain_id_and_without_parent_id(self): + # With restricted multi LDAP, tests that don't use identity, but do + # required aditional domains will work + base = super(BaseLDAPIdentity, self) + base.test_create_project_with_domain_id_and_without_parent_id() + + def test_create_project_with_domain_id_mismatch_to_parent_domain(self): + # With restricted multi LDAP, tests that don't use identity, but do + # required aditional domains will work + base = super(BaseLDAPIdentity, self) + base.test_create_project_with_domain_id_mismatch_to_parent_domain() + class DomainSpecificSQLIdentity(DomainSpecificLDAPandSQLIdentity): """Class to test simplest use of domain-specific SQL driver. @@ -3236,6 +3164,7 @@ class DomainSpecificSQLIdentity(DomainSpecificLDAPandSQLIdentity): - A separate SQL backend for domain1 """ + def initial_setup(self, sqldb): # We aren't setting up any initial data ahead of switching to # domain-specific operation, so make the switch straight away. @@ -3323,7 +3252,7 @@ class DomainSpecificSQLIdentity(DomainSpecificLDAPandSQLIdentity): 'domain2') -class LdapFilterTests(test_backend.FilterTests, unit.TestCase): +class LdapFilterTests(identity_tests.FilterTests, unit.TestCase): def setUp(self): super(LdapFilterTests, self).setUp() @@ -3333,7 +3262,7 @@ class LdapFilterTests(test_backend.FilterTests, unit.TestCase): self.load_backends() self.load_fixtures(default_fixtures) sqldb.recreate() - _assert_backends(self, assignment='ldap', identity='ldap') + _assert_backends(self, identity='ldap') def config_overrides(self): super(LdapFilterTests, self).config_overrides() @@ -3344,13 +3273,15 @@ class LdapFilterTests(test_backend.FilterTests, unit.TestCase): config_files.append(unit.dirs.tests_conf('backend_ldap.conf')) return config_files - def test_list_users_in_group_filtered(self): + @wip('Not supported by LDAP identity driver') + def test_list_users_in_group_inexact_filtered(self): + # The LDAP identity driver currently does not support filtering on the + # listing users for a given group, so will fail this test. + super(LdapFilterTests, + self).test_list_users_in_group_inexact_filtered() + + @wip('Not supported by LDAP identity driver') + def test_list_users_in_group_exact_filtered(self): # The LDAP identity driver currently does not support filtering on the # listing users for a given group, so will fail this test. - try: - super(LdapFilterTests, self).test_list_users_in_group_filtered() - except matchers.MismatchError: - return - # We shouldn't get here...if we do, it means someone has implemented - # filtering, so we can remove this test override. - self.assertTrue(False) + super(LdapFilterTests, self).test_list_users_in_group_exact_filtered() diff --git a/keystone-moon/keystone/tests/unit/test_backend_ldap_pool.py b/keystone-moon/keystone/tests/unit/test_backend_ldap_pool.py index 2b714b57..ec789d04 100644 --- a/keystone-moon/keystone/tests/unit/test_backend_ldap_pool.py +++ b/keystone-moon/keystone/tests/unit/test_backend_ldap_pool.py @@ -38,7 +38,7 @@ class LdapPoolCommonTestMixin(object): # by default use_pool and use_auth_pool is enabled in test pool config user_ref = self.identity_api.get_user(self.user_foo['id']) self.user_foo.pop('password') - self.assertDictEqual(user_ref, self.user_foo) + self.assertDictEqual(self.user_foo, user_ref) handler = ldap_core._get_connection(CONF.ldap.url, use_pool=True) self.assertIsInstance(handler, ldap_core.PooledLDAPHandler) @@ -151,22 +151,22 @@ class LdapPoolCommonTestMixin(object): # Open 3 connections first with _get_conn() as _: # conn1 - self.assertEqual(len(ldappool_cm), 1) + self.assertEqual(1, len(ldappool_cm)) with _get_conn() as _: # conn2 - self.assertEqual(len(ldappool_cm), 2) + self.assertEqual(2, len(ldappool_cm)) with _get_conn() as _: # conn2 _.unbind_ext_s() - self.assertEqual(len(ldappool_cm), 3) + self.assertEqual(3, len(ldappool_cm)) # Then open 3 connections again and make sure size does not grow # over 3 with _get_conn() as _: # conn1 - self.assertEqual(len(ldappool_cm), 1) + self.assertEqual(1, len(ldappool_cm)) with _get_conn() as _: # conn2 - self.assertEqual(len(ldappool_cm), 2) + self.assertEqual(2, len(ldappool_cm)) with _get_conn() as _: # conn3 _.unbind_ext_s() - self.assertEqual(len(ldappool_cm), 3) + self.assertEqual(3, len(ldappool_cm)) def test_password_change_with_pool(self): old_password = self.user_sna['password'] @@ -181,14 +181,14 @@ class LdapPoolCommonTestMixin(object): self.user_sna.pop('password') self.user_sna['enabled'] = True - self.assertDictEqual(user_ref, self.user_sna) + self.assertDictEqual(self.user_sna, user_ref) new_password = 'new_password' user_ref['password'] = new_password self.identity_api.update_user(user_ref['id'], user_ref) # now authenticate again to make sure new password works with - # conneciton pool + # connection pool user_ref2 = self.identity_api.authenticate( context={}, user_id=self.user_sna['id'], @@ -207,14 +207,15 @@ class LdapPoolCommonTestMixin(object): password=old_password) -class LdapIdentitySqlAssignment(LdapPoolCommonTestMixin, - test_backend_ldap.LdapIdentitySqlAssignment, - unit.TestCase): +class LDAPIdentity(LdapPoolCommonTestMixin, + test_backend_ldap.LDAPIdentity, + unit.TestCase): """Executes tests in existing base class with pooled LDAP handler.""" + def setUp(self): self.useFixture(mockpatch.PatchObject( ldap_core.PooledLDAPHandler, 'Connector', fakeldap.FakeLdapPool)) - super(LdapIdentitySqlAssignment, self).setUp() + super(LDAPIdentity, self).setUp() self.addCleanup(self.cleanup_pools) # storing to local variable to avoid long references @@ -225,7 +226,7 @@ class LdapIdentitySqlAssignment(LdapPoolCommonTestMixin, self.identity_api.get_user(self.user_foo['id']) def config_files(self): - config_files = super(LdapIdentitySqlAssignment, self).config_files() + config_files = super(LDAPIdentity, self).config_files() config_files.append(unit.dirs.tests_conf('backend_ldap_pool.conf')) return config_files diff --git a/keystone-moon/keystone/tests/unit/test_backend_rules.py b/keystone-moon/keystone/tests/unit/test_backend_rules.py index 9a11fddc..c32c3307 100644 --- a/keystone-moon/keystone/tests/unit/test_backend_rules.py +++ b/keystone-moon/keystone/tests/unit/test_backend_rules.py @@ -15,10 +15,10 @@ from keystone import exception from keystone.tests import unit -from keystone.tests.unit import test_backend +from keystone.tests.unit.policy import test_backends as policy_tests -class RulesPolicy(unit.TestCase, test_backend.PolicyTests): +class RulesPolicy(unit.TestCase, policy_tests.PolicyTests): def setUp(self): super(RulesPolicy, self).setUp() self.load_backends() @@ -47,14 +47,17 @@ class RulesPolicy(unit.TestCase, test_backend.PolicyTests): self.assertRaises(exception.NotImplemented, super(RulesPolicy, self).test_delete) - def test_get_policy_404(self): + def test_get_policy_returns_not_found(self): self.assertRaises(exception.NotImplemented, - super(RulesPolicy, self).test_get_policy_404) + super(RulesPolicy, + self).test_get_policy_returns_not_found) - def test_update_policy_404(self): + def test_update_policy_returns_not_found(self): self.assertRaises(exception.NotImplemented, - super(RulesPolicy, self).test_update_policy_404) + super(RulesPolicy, + self).test_update_policy_returns_not_found) - def test_delete_policy_404(self): + def test_delete_policy_returns_not_found(self): self.assertRaises(exception.NotImplemented, - super(RulesPolicy, self).test_delete_policy_404) + super(RulesPolicy, + self).test_delete_policy_returns_not_found) diff --git a/keystone-moon/keystone/tests/unit/test_backend_sql.py b/keystone-moon/keystone/tests/unit/test_backend_sql.py index 69fac63a..2e703fff 100644 --- a/keystone-moon/keystone/tests/unit/test_backend_sql.py +++ b/keystone-moon/keystone/tests/unit/test_backend_sql.py @@ -29,22 +29,28 @@ from keystone.common import driver_hints from keystone.common import sql from keystone import exception from keystone.identity.backends import sql as identity_sql +from keystone import resource from keystone.tests import unit +from keystone.tests.unit.assignment import test_backends as assignment_tests +from keystone.tests.unit.catalog import test_backends as catalog_tests from keystone.tests.unit import default_fixtures +from keystone.tests.unit.identity import test_backends as identity_tests from keystone.tests.unit.ksfixtures import database -from keystone.tests.unit import test_backend +from keystone.tests.unit.policy import test_backends as policy_tests +from keystone.tests.unit.resource import test_backends as resource_tests +from keystone.tests.unit.token import test_backends as token_tests +from keystone.tests.unit.trust import test_backends as trust_tests from keystone.token.persistence.backends import sql as token_sql CONF = cfg.CONF -DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id class SqlTests(unit.SQLDriverOverrides, unit.TestCase): def setUp(self): super(SqlTests, self).setUp() - self.useFixture(database.Database()) + self.useFixture(database.Database(self.sql_driver_version_overrides)) self.load_backends() # populate the engine with tables & fixtures @@ -124,14 +130,33 @@ class SqlModels(SqlTests): def test_user_model(self): cols = (('id', sql.String, 64), - ('name', sql.String, 255), - ('password', sql.String, 128), - ('domain_id', sql.String, 64), ('default_project_id', sql.String, 64), ('enabled', sql.Boolean, None), ('extra', sql.JsonBlob, None)) self.assertExpectedSchema('user', cols) + def test_local_user_model(self): + cols = (('id', sql.Integer, None), + ('user_id', sql.String, 64), + ('name', sql.String, 255), + ('domain_id', sql.String, 64)) + self.assertExpectedSchema('local_user', cols) + + def test_password_model(self): + cols = (('id', sql.Integer, None), + ('local_user_id', sql.Integer, None), + ('password', sql.String, 128)) + self.assertExpectedSchema('password', cols) + + def test_federated_user_model(self): + cols = (('id', sql.Integer, None), + ('user_id', sql.String, 64), + ('idp_id', sql.String, 64), + ('protocol_id', sql.String, 64), + ('unique_id', sql.String, 255), + ('display_name', sql.String, 255)) + self.assertExpectedSchema('federated_user', cols) + def test_group_model(self): cols = (('id', sql.String, 64), ('name', sql.String, 64), @@ -171,17 +196,58 @@ class SqlModels(SqlTests): ('user_id', sql.String, 64)) self.assertExpectedSchema('user_group_membership', cols) - -class SqlIdentity(SqlTests, test_backend.IdentityTests): + def test_revocation_event_model(self): + cols = (('id', sql.Integer, None), + ('domain_id', sql.String, 64), + ('project_id', sql.String, 64), + ('user_id', sql.String, 64), + ('role_id', sql.String, 64), + ('trust_id', sql.String, 64), + ('consumer_id', sql.String, 64), + ('access_token_id', sql.String, 64), + ('issued_before', sql.DateTime, None), + ('expires_at', sql.DateTime, None), + ('revoked_at', sql.DateTime, None), + ('audit_id', sql.String, 32), + ('audit_chain_id', sql.String, 32)) + self.assertExpectedSchema('revocation_event', cols) + + +class SqlIdentity(SqlTests, identity_tests.IdentityTests, + assignment_tests.AssignmentTests, + resource_tests.ResourceTests): def test_password_hashed(self): - session = sql.get_session() - user_ref = self.identity_api._get_user(session, self.user_foo['id']) - self.assertNotEqual(user_ref['password'], self.user_foo['password']) + with sql.session_for_read() as session: + user_ref = self.identity_api._get_user(session, + self.user_foo['id']) + self.assertNotEqual(self.user_foo['password'], + user_ref['password']) + + def test_create_user_with_null_password(self): + user_dict = unit.new_user_ref( + domain_id=CONF.identity.default_domain_id) + user_dict["password"] = None + new_user_dict = self.identity_api.create_user(user_dict) + with sql.session_for_read() as session: + new_user_ref = self.identity_api._get_user(session, + new_user_dict['id']) + self.assertFalse(new_user_ref.local_user.passwords) + + def test_update_user_with_null_password(self): + user_dict = unit.new_user_ref( + domain_id=CONF.identity.default_domain_id) + self.assertTrue(user_dict['password']) + new_user_dict = self.identity_api.create_user(user_dict) + new_user_dict["password"] = None + new_user_dict = self.identity_api.update_user(new_user_dict['id'], + new_user_dict) + with sql.session_for_read() as session: + new_user_ref = self.identity_api._get_user(session, + new_user_dict['id']) + self.assertFalse(new_user_ref.local_user.passwords) def test_delete_user_with_project_association(self): - user = {'name': uuid.uuid4().hex, - 'domain_id': DEFAULT_DOMAIN_ID, - 'password': uuid.uuid4().hex} + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) user = self.identity_api.create_user(user) self.assignment_api.add_user_to_project(self.tenant_bar['id'], user['id']) @@ -191,16 +257,15 @@ class SqlIdentity(SqlTests, test_backend.IdentityTests): user['id']) def test_create_null_user_name(self): - user = {'name': None, - 'domain_id': DEFAULT_DOMAIN_ID, - 'password': uuid.uuid4().hex} + user = unit.new_user_ref(name=None, + domain_id=CONF.identity.default_domain_id) self.assertRaises(exception.ValidationError, self.identity_api.create_user, user) self.assertRaises(exception.UserNotFound, self.identity_api.get_user_by_name, user['name'], - DEFAULT_DOMAIN_ID) + CONF.identity.default_domain_id) def test_create_user_case_sensitivity(self): # user name case sensitivity is down to the fact that it is marked as @@ -208,25 +273,59 @@ class SqlIdentity(SqlTests, test_backend.IdentityTests): # LDAP. # create a ref with a lowercase name - ref = { - 'name': uuid.uuid4().hex.lower(), - 'domain_id': DEFAULT_DOMAIN_ID} + ref = unit.new_user_ref(name=uuid.uuid4().hex.lower(), + domain_id=CONF.identity.default_domain_id) ref = self.identity_api.create_user(ref) # assign a new ID with the same name, but this time in uppercase ref['name'] = ref['name'].upper() self.identity_api.create_user(ref) + def test_create_federated_user_unique_constraint(self): + federated_dict = unit.new_federated_user_ref() + user_dict = self.shadow_users_api.create_federated_user(federated_dict) + user_dict = self.identity_api.get_user(user_dict["id"]) + self.assertIsNotNone(user_dict["id"]) + self.assertRaises(exception.Conflict, + self.shadow_users_api.create_federated_user, + federated_dict) + + def test_get_federated_user(self): + federated_dict = unit.new_federated_user_ref() + user_dict_create = self.shadow_users_api.create_federated_user( + federated_dict) + user_dict_get = self.shadow_users_api.get_federated_user( + federated_dict["idp_id"], + federated_dict["protocol_id"], + federated_dict["unique_id"]) + self.assertItemsEqual(user_dict_create, user_dict_get) + self.assertEqual(user_dict_create["id"], user_dict_get["id"]) + + def test_update_federated_user_display_name(self): + federated_dict = unit.new_federated_user_ref() + user_dict_create = self.shadow_users_api.create_federated_user( + federated_dict) + new_display_name = uuid.uuid4().hex + self.shadow_users_api.update_federated_user_display_name( + federated_dict["idp_id"], + federated_dict["protocol_id"], + federated_dict["unique_id"], + new_display_name) + user_ref = self.shadow_users_api._get_federated_user( + federated_dict["idp_id"], + federated_dict["protocol_id"], + federated_dict["unique_id"]) + self.assertEqual(user_ref.federated_users[0].display_name, + new_display_name) + self.assertEqual(user_dict_create["id"], user_ref.id) + def test_create_project_case_sensitivity(self): # project name case sensitivity is down to the fact that it is marked # as an SQL UNIQUE column, which may not be valid for other backends, # like LDAP. # create a ref with a lowercase name - ref = { - 'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex.lower(), - 'domain_id': DEFAULT_DOMAIN_ID} + ref = unit.new_project_ref(domain_id=CONF.identity.default_domain_id) self.resource_api.create_project(ref['id'], ref) # assign a new ID with the same name, but this time in uppercase @@ -235,25 +334,22 @@ class SqlIdentity(SqlTests, test_backend.IdentityTests): self.resource_api.create_project(ref['id'], ref) def test_create_null_project_name(self): - tenant = {'id': uuid.uuid4().hex, - 'name': None, - 'domain_id': DEFAULT_DOMAIN_ID} + project = unit.new_project_ref( + name=None, domain_id=CONF.identity.default_domain_id) self.assertRaises(exception.ValidationError, self.resource_api.create_project, - tenant['id'], - tenant) + project['id'], + project) self.assertRaises(exception.ProjectNotFound, self.resource_api.get_project, - tenant['id']) + project['id']) self.assertRaises(exception.ProjectNotFound, self.resource_api.get_project_by_name, - tenant['name'], - DEFAULT_DOMAIN_ID) + project['name'], + CONF.identity.default_domain_id) def test_delete_project_with_user_association(self): - user = {'name': 'fakeuser', - 'domain_id': DEFAULT_DOMAIN_ID, - 'password': 'passwd'} + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) user = self.identity_api.create_user(user) self.assignment_api.add_user_to_project(self.tenant_bar['id'], user['id']) @@ -261,52 +357,6 @@ class SqlIdentity(SqlTests, test_backend.IdentityTests): tenants = self.assignment_api.list_projects_for_user(user['id']) self.assertEqual([], tenants) - def test_metadata_removed_on_delete_user(self): - # A test to check that the internal representation - # or roles is correctly updated when a user is deleted - user = {'name': uuid.uuid4().hex, - 'domain_id': DEFAULT_DOMAIN_ID, - 'password': 'passwd'} - user = self.identity_api.create_user(user) - role = {'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex} - self.role_api.create_role(role['id'], role) - self.assignment_api.add_role_to_user_and_project( - user['id'], - self.tenant_bar['id'], - role['id']) - self.identity_api.delete_user(user['id']) - - # Now check whether the internal representation of roles - # has been deleted - self.assertRaises(exception.MetadataNotFound, - self.assignment_api._get_metadata, - user['id'], - self.tenant_bar['id']) - - def test_metadata_removed_on_delete_project(self): - # A test to check that the internal representation - # or roles is correctly updated when a project is deleted - user = {'name': uuid.uuid4().hex, - 'domain_id': DEFAULT_DOMAIN_ID, - 'password': 'passwd'} - user = self.identity_api.create_user(user) - role = {'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex} - self.role_api.create_role(role['id'], role) - self.assignment_api.add_role_to_user_and_project( - user['id'], - self.tenant_bar['id'], - role['id']) - self.resource_api.delete_project(self.tenant_bar['id']) - - # Now check whether the internal representation of roles - # has been deleted - self.assertRaises(exception.MetadataNotFound, - self.assignment_api._get_metadata, - user['id'], - self.tenant_bar['id']) - def test_update_project_returns_extra(self): """This tests for backwards-compatibility with an essex/folsom bug. @@ -317,20 +367,17 @@ class SqlIdentity(SqlTests, test_backend.IdentityTests): This behavior is specific to the SQL driver. """ - tenant_id = uuid.uuid4().hex arbitrary_key = uuid.uuid4().hex arbitrary_value = uuid.uuid4().hex - tenant = { - 'id': tenant_id, - 'name': uuid.uuid4().hex, - 'domain_id': DEFAULT_DOMAIN_ID, - arbitrary_key: arbitrary_value} - ref = self.resource_api.create_project(tenant_id, tenant) + project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + project[arbitrary_key] = arbitrary_value + ref = self.resource_api.create_project(project['id'], project) self.assertEqual(arbitrary_value, ref[arbitrary_key]) self.assertIsNone(ref.get('extra')) - tenant['name'] = uuid.uuid4().hex - ref = self.resource_api.update_project(tenant_id, tenant) + ref['name'] = uuid.uuid4().hex + ref = self.resource_api.update_project(ref['id'], ref) self.assertEqual(arbitrary_value, ref[arbitrary_key]) self.assertEqual(arbitrary_value, ref['extra'][arbitrary_key]) @@ -346,11 +393,9 @@ class SqlIdentity(SqlTests, test_backend.IdentityTests): """ arbitrary_key = uuid.uuid4().hex arbitrary_value = uuid.uuid4().hex - user = { - 'name': uuid.uuid4().hex, - 'domain_id': DEFAULT_DOMAIN_ID, - 'password': uuid.uuid4().hex, - arbitrary_key: arbitrary_value} + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) + user[arbitrary_key] = arbitrary_value + del user["id"] ref = self.identity_api.create_user(user) self.assertEqual(arbitrary_value, ref[arbitrary_key]) self.assertIsNone(ref.get('password')) @@ -365,30 +410,25 @@ class SqlIdentity(SqlTests, test_backend.IdentityTests): self.assertEqual(arbitrary_value, ref['extra'][arbitrary_key]) def test_sql_user_to_dict_null_default_project_id(self): - user = { - 'name': uuid.uuid4().hex, - 'domain_id': DEFAULT_DOMAIN_ID, - 'password': uuid.uuid4().hex} - + user = unit.new_user_ref(domain_id=CONF.identity.default_domain_id) user = self.identity_api.create_user(user) - session = sql.get_session() - query = session.query(identity_sql.User) - query = query.filter_by(id=user['id']) - raw_user_ref = query.one() - self.assertIsNone(raw_user_ref.default_project_id) - user_ref = raw_user_ref.to_dict() - self.assertNotIn('default_project_id', user_ref) - session.close() + with sql.session_for_read() as session: + query = session.query(identity_sql.User) + query = query.filter_by(id=user['id']) + raw_user_ref = query.one() + self.assertIsNone(raw_user_ref.default_project_id) + user_ref = raw_user_ref.to_dict() + self.assertNotIn('default_project_id', user_ref) + session.close() def test_list_domains_for_user(self): - domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + domain = unit.new_domain_ref() self.resource_api.create_domain(domain['id'], domain) - user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, - 'domain_id': domain['id'], 'enabled': True} + user = unit.new_user_ref(domain_id=domain['id']) - test_domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + test_domain1 = unit.new_domain_ref() self.resource_api.create_domain(test_domain1['id'], test_domain1) - test_domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + test_domain2 = unit.new_domain_ref() self.resource_api.create_domain(test_domain2['id'], test_domain2) user = self.identity_api.create_user(user) @@ -407,21 +447,20 @@ class SqlIdentity(SqlTests, test_backend.IdentityTests): # Create two groups each with a role on a different domain, and # make user1 a member of both groups. Both these new domains # should now be included, along with any direct user grants. - domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + domain = unit.new_domain_ref() self.resource_api.create_domain(domain['id'], domain) - user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, - 'domain_id': domain['id'], 'enabled': True} + user = unit.new_user_ref(domain_id=domain['id']) user = self.identity_api.create_user(user) - group1 = {'name': uuid.uuid4().hex, 'domain_id': domain['id']} + group1 = unit.new_group_ref(domain_id=domain['id']) group1 = self.identity_api.create_group(group1) - group2 = {'name': uuid.uuid4().hex, 'domain_id': domain['id']} + group2 = unit.new_group_ref(domain_id=domain['id']) group2 = self.identity_api.create_group(group2) - test_domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + test_domain1 = unit.new_domain_ref() self.resource_api.create_domain(test_domain1['id'], test_domain1) - test_domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + test_domain2 = unit.new_domain_ref() self.resource_api.create_domain(test_domain2['id'], test_domain2) - test_domain3 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + test_domain3 = unit.new_domain_ref() self.resource_api.create_domain(test_domain3['id'], test_domain3) self.identity_api.add_user_to_group(user['id'], group1['id']) @@ -451,17 +490,16 @@ class SqlIdentity(SqlTests, test_backend.IdentityTests): - When listing domains for user, neither domain should be returned """ - domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + domain1 = unit.new_domain_ref() domain1 = self.resource_api.create_domain(domain1['id'], domain1) - domain2 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + domain2 = unit.new_domain_ref() domain2 = self.resource_api.create_domain(domain2['id'], domain2) - user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, - 'domain_id': domain1['id'], 'enabled': True} + user = unit.new_user_ref(domain_id=domain1['id']) user = self.identity_api.create_user(user) - group = {'name': uuid.uuid4().hex, 'domain_id': domain1['id']} + group = unit.new_group_ref(domain_id=domain1['id']) group = self.identity_api.create_group(group) self.identity_api.add_user_to_group(user['id'], group['id']) - role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + role = unit.new_role_ref() self.role_api.create_role(role['id'], role) # Create a grant on each domain, one user grant, one group grant, @@ -480,25 +518,143 @@ class SqlIdentity(SqlTests, test_backend.IdentityTests): # roles assignments. self.assertThat(user_domains, matchers.HasLength(0)) + def test_storing_null_domain_id_in_project_ref(self): + """Test the special storage of domain_id=None in sql resource driver. + + The resource driver uses a special value in place of None for domain_id + in the project record. This shouldn't escape the driver. Hence we test + the interface to ensure that you can store a domain_id of None, and + that any special value used inside the driver does not escape through + the interface. + + """ + spoiler_project = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + self.resource_api.create_project(spoiler_project['id'], + spoiler_project) + + # First let's create a project with a None domain_id and make sure we + # can read it back. + project = unit.new_project_ref(domain_id=None, is_domain=True) + project = self.resource_api.create_project(project['id'], project) + ref = self.resource_api.get_project(project['id']) + self.assertDictEqual(project, ref) + + # Can we get it by name? + ref = self.resource_api.get_project_by_name(project['name'], None) + self.assertDictEqual(project, ref) + + # Can we filter for them - create a second domain to ensure we are + # testing the receipt of more than one. + project2 = unit.new_project_ref(domain_id=None, is_domain=True) + project2 = self.resource_api.create_project(project2['id'], project2) + hints = driver_hints.Hints() + hints.add_filter('domain_id', None) + refs = self.resource_api.list_projects(hints) + self.assertThat(refs, matchers.HasLength(2 + self.domain_count)) + self.assertIn(project, refs) + self.assertIn(project2, refs) + + # Can we update it? + project['name'] = uuid.uuid4().hex + self.resource_api.update_project(project['id'], project) + ref = self.resource_api.get_project(project['id']) + self.assertDictEqual(project, ref) + + # Finally, make sure we can delete it + project['enabled'] = False + self.resource_api.update_project(project['id'], project) + self.resource_api.delete_project(project['id']) + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + project['id']) + + def test_hidden_project_domain_root_is_really_hidden(self): + """Ensure we cannot access the hidden root of all project domains. + + Calling any of the driver methods should result in the same as + would be returned if we passed a project that does not exist. We don't + test create_project, since we do not allow a caller of our API to + specify their own ID for a new entity. + + """ + def _exercise_project_api(ref_id): + driver = self.resource_api.driver + self.assertRaises(exception.ProjectNotFound, + driver.get_project, + ref_id) + + self.assertRaises(exception.ProjectNotFound, + driver.get_project_by_name, + resource.NULL_DOMAIN_ID, + ref_id) + + project_ids = [x['id'] for x in + driver.list_projects(driver_hints.Hints())] + self.assertNotIn(ref_id, project_ids) + + projects = driver.list_projects_from_ids([ref_id]) + self.assertThat(projects, matchers.HasLength(0)) -class SqlTrust(SqlTests, test_backend.TrustTests): + project_ids = [x for x in + driver.list_project_ids_from_domain_ids([ref_id])] + self.assertNotIn(ref_id, project_ids) + + self.assertRaises(exception.DomainNotFound, + driver.list_projects_in_domain, + ref_id) + + project_ids = [ + x['id'] for x in + driver.list_projects_acting_as_domain(driver_hints.Hints())] + self.assertNotIn(ref_id, project_ids) + + projects = driver.list_projects_in_subtree(ref_id) + self.assertThat(projects, matchers.HasLength(0)) + + self.assertRaises(exception.ProjectNotFound, + driver.list_project_parents, + ref_id) + + # A non-existing project just returns True from the driver + self.assertTrue(driver.is_leaf_project(ref_id)) + + self.assertRaises(exception.ProjectNotFound, + driver.update_project, + ref_id, + {}) + + self.assertRaises(exception.ProjectNotFound, + driver.delete_project, + ref_id) + + # Deleting list of projects that includes a non-existing project + # should be silent + driver.delete_projects_from_ids([ref_id]) + + _exercise_project_api(uuid.uuid4().hex) + _exercise_project_api(resource.NULL_DOMAIN_ID) + + +class SqlTrust(SqlTests, trust_tests.TrustTests): pass -class SqlToken(SqlTests, test_backend.TokenTests): +class SqlToken(SqlTests, token_tests.TokenTests): def test_token_revocation_list_uses_right_columns(self): # This query used to be heavy with too many columns. We want # to make sure it is only running with the minimum columns # necessary. expected_query_args = (token_sql.TokenModel.id, - token_sql.TokenModel.expires) + token_sql.TokenModel.expires, + token_sql.TokenModel.extra,) with mock.patch.object(token_sql, 'sql') as mock_sql: tok = token_sql.Token() tok.list_revoked_tokens() - mock_query = mock_sql.get_session().query + mock_query = mock_sql.session_for_read().__enter__().query mock_query.assert_called_with(*expected_query_args) def test_flush_expired_tokens_batch(self): @@ -523,8 +679,12 @@ class SqlToken(SqlTests, test_backend.TokenTests): # other tests below test the differences between how they use the batch # strategy with mock.patch.object(token_sql, 'sql') as mock_sql: - mock_sql.get_session().query().filter().delete.return_value = 0 - mock_sql.get_session().bind.dialect.name = 'mysql' + mock_sql.session_for_write().__enter__( + ).query().filter().delete.return_value = 0 + + mock_sql.session_for_write().__enter__( + ).bind.dialect.name = 'mysql' + tok = token_sql.Token() expiry_mock = mock.Mock() ITERS = [1, 2, 3] @@ -535,7 +695,10 @@ class SqlToken(SqlTests, test_backend.TokenTests): # The expiry strategy is only invoked once, the other calls are via # the yield return. self.assertEqual(1, expiry_mock.call_count) - mock_delete = mock_sql.get_session().query().filter().delete + + mock_delete = mock_sql.session_for_write().__enter__( + ).query().filter().delete + self.assertThat(mock_delete.call_args_list, matchers.HasLength(len(ITERS))) @@ -550,12 +713,12 @@ class SqlToken(SqlTests, test_backend.TokenTests): if i == 0: # The first time the batch iterator returns, it should return # the first result that comes back from the database. - self.assertEqual(x, 'test') + self.assertEqual('test', x) elif i == 1: # The second time, the database range function should return # nothing, so the batch iterator returns the result of the # upper_bound function - self.assertEqual(x, "final value") + self.assertEqual("final value", x) else: self.fail("range batch function returned more than twice") @@ -568,39 +731,30 @@ class SqlToken(SqlTests, test_backend.TokenTests): tok = token_sql.Token() db2_strategy = tok._expiry_range_strategy('ibm_db_sa') self.assertIsInstance(db2_strategy, functools.partial) - self.assertEqual(db2_strategy.func, token_sql._expiry_range_batched) - self.assertEqual(db2_strategy.keywords, {'batch_size': 100}) + self.assertEqual(token_sql._expiry_range_batched, db2_strategy.func) + self.assertEqual({'batch_size': 100}, db2_strategy.keywords) def test_expiry_range_strategy_mysql(self): tok = token_sql.Token() mysql_strategy = tok._expiry_range_strategy('mysql') self.assertIsInstance(mysql_strategy, functools.partial) - self.assertEqual(mysql_strategy.func, token_sql._expiry_range_batched) - self.assertEqual(mysql_strategy.keywords, {'batch_size': 1000}) + self.assertEqual(token_sql._expiry_range_batched, mysql_strategy.func) + self.assertEqual({'batch_size': 1000}, mysql_strategy.keywords) -class SqlCatalog(SqlTests, test_backend.CatalogTests): +class SqlCatalog(SqlTests, catalog_tests.CatalogTests): _legacy_endpoint_id_in_endpoint = True _enabled_default_to_true_when_creating_endpoint = True def test_catalog_ignored_malformed_urls(self): - service = { - 'id': uuid.uuid4().hex, - 'type': uuid.uuid4().hex, - 'name': uuid.uuid4().hex, - 'description': uuid.uuid4().hex, - } - self.catalog_api.create_service(service['id'], service.copy()) + service = unit.new_service_ref() + self.catalog_api.create_service(service['id'], service) malformed_url = "http://192.168.1.104:8774/v2/$(tenant)s" - endpoint = { - 'id': uuid.uuid4().hex, - 'region_id': None, - 'service_id': service['id'], - 'interface': 'public', - 'url': malformed_url, - } + endpoint = unit.new_endpoint_ref(service_id=service['id'], + url=malformed_url, + region_id=None) self.catalog_api.create_endpoint(endpoint['id'], endpoint.copy()) # NOTE(dstanek): there are no valid URLs, so nothing is in the catalog @@ -608,21 +762,11 @@ class SqlCatalog(SqlTests, test_backend.CatalogTests): self.assertEqual({}, catalog) def test_get_catalog_with_empty_public_url(self): - service = { - 'id': uuid.uuid4().hex, - 'type': uuid.uuid4().hex, - 'name': uuid.uuid4().hex, - 'description': uuid.uuid4().hex, - } - self.catalog_api.create_service(service['id'], service.copy()) - - endpoint = { - 'id': uuid.uuid4().hex, - 'region_id': None, - 'interface': 'public', - 'url': '', - 'service_id': service['id'], - } + service = unit.new_service_ref() + self.catalog_api.create_service(service['id'], service) + + endpoint = unit.new_endpoint_ref(url='', service_id=service['id'], + region_id=None) self.catalog_api.create_endpoint(endpoint['id'], endpoint.copy()) catalog = self.catalog_api.get_catalog('user', 'tenant') @@ -633,22 +777,12 @@ class SqlCatalog(SqlTests, test_backend.CatalogTests): self.assertIsNone(catalog_endpoint.get('adminURL')) self.assertIsNone(catalog_endpoint.get('internalURL')) - def test_create_endpoint_region_404(self): - service = { - 'id': uuid.uuid4().hex, - 'type': uuid.uuid4().hex, - 'name': uuid.uuid4().hex, - 'description': uuid.uuid4().hex, - } - self.catalog_api.create_service(service['id'], service.copy()) - - endpoint = { - 'id': uuid.uuid4().hex, - 'region_id': uuid.uuid4().hex, - 'service_id': service['id'], - 'interface': 'public', - 'url': uuid.uuid4().hex, - } + def test_create_endpoint_region_returns_not_found(self): + service = unit.new_service_ref() + self.catalog_api.create_service(service['id'], service) + + endpoint = unit.new_endpoint_ref(region_id=uuid.uuid4().hex, + service_id=service['id']) self.assertRaises(exception.ValidationError, self.catalog_api.create_endpoint, @@ -656,21 +790,14 @@ class SqlCatalog(SqlTests, test_backend.CatalogTests): endpoint.copy()) def test_create_region_invalid_id(self): - region = { - 'id': '0' * 256, - 'description': '', - 'extra': {}, - } + region = unit.new_region_ref(id='0' * 256) self.assertRaises(exception.StringLengthExceeded, self.catalog_api.create_region, - region.copy()) + region) def test_create_region_invalid_parent_id(self): - region = { - 'id': uuid.uuid4().hex, - 'parent_region_id': '0' * 256, - } + region = unit.new_region_ref(parent_region_id='0' * 256) self.assertRaises(exception.RegionNotFound, self.catalog_api.create_region, @@ -678,77 +805,57 @@ class SqlCatalog(SqlTests, test_backend.CatalogTests): def test_delete_region_with_endpoint(self): # create a region - region = { - 'id': uuid.uuid4().hex, - 'description': uuid.uuid4().hex, - } + region = unit.new_region_ref() self.catalog_api.create_region(region) # create a child region - child_region = { - 'id': uuid.uuid4().hex, - 'description': uuid.uuid4().hex, - 'parent_id': region['id'] - } + child_region = unit.new_region_ref(parent_region_id=region['id']) self.catalog_api.create_region(child_region) # create a service - service = { - 'id': uuid.uuid4().hex, - 'type': uuid.uuid4().hex, - 'name': uuid.uuid4().hex, - 'description': uuid.uuid4().hex, - } + service = unit.new_service_ref() self.catalog_api.create_service(service['id'], service) # create an endpoint attached to the service and child region - child_endpoint = { - 'id': uuid.uuid4().hex, - 'region_id': child_region['id'], - 'interface': uuid.uuid4().hex[:8], - 'url': uuid.uuid4().hex, - 'service_id': service['id'], - } + child_endpoint = unit.new_endpoint_ref(region_id=child_region['id'], + service_id=service['id']) + self.catalog_api.create_endpoint(child_endpoint['id'], child_endpoint) self.assertRaises(exception.RegionDeletionError, self.catalog_api.delete_region, child_region['id']) # create an endpoint attached to the service and parent region - endpoint = { - 'id': uuid.uuid4().hex, - 'region_id': region['id'], - 'interface': uuid.uuid4().hex[:8], - 'url': uuid.uuid4().hex, - 'service_id': service['id'], - } + endpoint = unit.new_endpoint_ref(region_id=region['id'], + service_id=service['id']) + self.catalog_api.create_endpoint(endpoint['id'], endpoint) self.assertRaises(exception.RegionDeletionError, self.catalog_api.delete_region, region['id']) -class SqlPolicy(SqlTests, test_backend.PolicyTests): +class SqlPolicy(SqlTests, policy_tests.PolicyTests): pass -class SqlInheritance(SqlTests, test_backend.InheritanceTests): +class SqlInheritance(SqlTests, assignment_tests.InheritanceTests): pass -class SqlTokenCacheInvalidation(SqlTests, test_backend.TokenCacheInvalidation): +class SqlImpliedRoles(SqlTests, assignment_tests.ImpliedRoleTests): + pass + + +class SqlTokenCacheInvalidation(SqlTests, token_tests.TokenCacheInvalidation): def setUp(self): super(SqlTokenCacheInvalidation, self).setUp() self._create_test_data() -class SqlFilterTests(SqlTests, test_backend.FilterTests): - - def _get_user_name_field_size(self): - return identity_sql.User.name.type.length +class SqlFilterTests(SqlTests, identity_tests.FilterTests): def clean_up_entities(self): """Clean up entity test data from Filter Test Cases.""" - for entity in ['user', 'group', 'project']: self._delete_test_data(entity, self.entity_list[entity]) self._delete_test_data(entity, self.domain1_entity_list[entity]) @@ -760,11 +867,12 @@ class SqlFilterTests(SqlTests, test_backend.FilterTests): del self.domain1 def test_list_entities_filtered_by_domain(self): - # NOTE(henry-nash): This method is here rather than in test_backend - # since any domain filtering with LDAP is handled by the manager - # layer (and is already tested elsewhere) not at the driver level. + # NOTE(henry-nash): This method is here rather than in + # unit.identity.test_backends since any domain filtering with LDAP is + # handled by the manager layer (and is already tested elsewhere) not at + # the driver level. self.addCleanup(self.clean_up_entities) - self.domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.domain1 = unit.new_domain_ref() self.resource_api.create_domain(self.domain1['id'], self.domain1) self.entity_list = {} @@ -804,7 +912,7 @@ class SqlFilterTests(SqlTests, test_backend.FilterTests): # See if we can add a SQL command...use the group table instead of the # user table since 'user' is reserved word for SQLAlchemy. - group = {'name': uuid.uuid4().hex, 'domain_id': DEFAULT_DOMAIN_ID} + group = unit.new_group_ref(domain_id=CONF.identity.default_domain_id) group = self.identity_api.create_group(group) hints = driver_hints.Hints() @@ -816,10 +924,10 @@ class SqlFilterTests(SqlTests, test_backend.FilterTests): self.assertTrue(len(groups) > 0) -class SqlLimitTests(SqlTests, test_backend.LimitTests): +class SqlLimitTests(SqlTests, identity_tests.LimitTests): def setUp(self): super(SqlLimitTests, self).setUp() - test_backend.LimitTests.setUp(self) + identity_tests.LimitTests.setUp(self) class FakeTable(sql.ModelBase): @@ -850,11 +958,6 @@ class SqlDecorators(unit.TestCase): tt = FakeTable(col='a') self.assertEqual('a', tt.col) - def test_non_ascii_init(self): - # NOTE(I159): Non ASCII characters must cause UnicodeDecodeError - # if encoding is not provided explicitly. - self.assertRaises(UnicodeDecodeError, FakeTable, col='Я') - def test_conflict_happend(self): self.assertRaises(exception.Conflict, FakeTable().insert) self.assertRaises(exception.UnexpectedError, FakeTable().update) @@ -876,21 +979,15 @@ class SqlModuleInitialization(unit.TestCase): class SqlCredential(SqlTests): def _create_credential_with_user_id(self, user_id=uuid.uuid4().hex): - credential_id = uuid.uuid4().hex - new_credential = { - 'id': credential_id, - 'user_id': user_id, - 'project_id': uuid.uuid4().hex, - 'blob': uuid.uuid4().hex, - 'type': uuid.uuid4().hex, - 'extra': uuid.uuid4().hex - } - self.credential_api.create_credential(credential_id, new_credential) - return new_credential + credential = unit.new_credential_ref(user_id=user_id, + extra=uuid.uuid4().hex, + type=uuid.uuid4().hex) + self.credential_api.create_credential(credential['id'], credential) + return credential def _validateCredentialList(self, retrieved_credentials, expected_credentials): - self.assertEqual(len(retrieved_credentials), len(expected_credentials)) + self.assertEqual(len(expected_credentials), len(retrieved_credentials)) retrived_ids = [c['id'] for c in retrieved_credentials] for cred in expected_credentials: self.assertIn(cred['id'], retrived_ids) @@ -920,3 +1017,9 @@ class SqlCredential(SqlTests): credentials = self.credential_api.list_credentials_for_user( self.user_foo['id']) self._validateCredentialList(credentials, self.user_credentials) + + def test_list_credentials_for_user_and_type(self): + cred = self.user_credentials[0] + credentials = self.credential_api.list_credentials_for_user( + self.user_foo['id'], type=cred['type']) + self._validateCredentialList(credentials, [cred]) diff --git a/keystone-moon/keystone/tests/unit/test_backend_templated.py b/keystone-moon/keystone/tests/unit/test_backend_templated.py index 4a7bf9e5..ca957e78 100644 --- a/keystone-moon/keystone/tests/unit/test_backend_templated.py +++ b/keystone-moon/keystone/tests/unit/test_backend_templated.py @@ -19,16 +19,16 @@ from six.moves import zip from keystone import catalog from keystone.tests import unit +from keystone.tests.unit.catalog import test_backends as catalog_tests from keystone.tests.unit import default_fixtures from keystone.tests.unit.ksfixtures import database -from keystone.tests.unit import test_backend BROKEN_WRITE_FUNCTIONALITY_MSG = ("Templated backend doesn't correctly " "implement write operations") -class TestTemplatedCatalog(unit.TestCase, test_backend.CatalogTests): +class TestTemplatedCatalog(unit.TestCase, catalog_tests.CatalogTests): DEFAULT_FIXTURE = { 'RegionOne': { @@ -64,8 +64,11 @@ class TestTemplatedCatalog(unit.TestCase, test_backend.CatalogTests): def test_get_catalog(self): catalog_ref = self.catalog_api.get_catalog('foo', 'bar') - self.assertDictEqual(catalog_ref, self.DEFAULT_FIXTURE) + self.assertDictEqual(self.DEFAULT_FIXTURE, catalog_ref) + # NOTE(lbragstad): This test is skipped because the catalog is being + # modified within the test and not through the API. + @unit.skip_if_cache_is_enabled('catalog') def test_catalog_ignored_malformed_urls(self): # both endpoints are in the catalog catalog_ref = self.catalog_api.get_catalog('foo', 'bar') @@ -85,7 +88,9 @@ class TestTemplatedCatalog(unit.TestCase, test_backend.CatalogTests): self.skipTest("Templated backend doesn't have disabled endpoints") def assert_catalogs_equal(self, expected, observed): - for e, o in zip(sorted(expected), sorted(observed)): + sort_key = lambda d: d['id'] + for e, o in zip(sorted(expected, key=sort_key), + sorted(observed, key=sort_key)): expected_endpoints = e.pop('endpoints') observed_endpoints = o.pop('endpoints') self.assertDictEqual(e, o) @@ -126,9 +131,10 @@ class TestTemplatedCatalog(unit.TestCase, test_backend.CatalogTests): def test_get_catalog_ignores_endpoints_with_invalid_urls(self): user_id = uuid.uuid4().hex + tenant_id = None # If the URL has no 'tenant_id' to substitute, we will skip the # endpoint which contains this kind of URL. - catalog_ref = self.catalog_api.get_v3_catalog(user_id, tenant_id=None) + catalog_ref = self.catalog_api.get_v3_catalog(user_id, tenant_id) exp_catalog = [ {'endpoints': [], 'type': 'compute', @@ -155,8 +161,24 @@ class TestTemplatedCatalog(unit.TestCase, test_backend.CatalogTests): def test_service_filtering(self): self.skipTest("Templated backend doesn't support filtering") + def test_list_services_with_hints(self): + hints = {} + services = self.catalog_api.list_services(hints=hints) + exp_services = [ + {'type': 'compute', + 'description': '', + 'enabled': True, + 'name': "'Compute Service'", + 'id': 'compute'}, + {'type': 'identity', + 'description': '', + 'enabled': True, + 'name': "'Identity Service'", + 'id': 'identity'}] + self.assertItemsEqual(exp_services, services) + # NOTE(dstanek): the following methods have been overridden - # from test_backend.CatalogTests + # from unit.catalog.test_backends.CatalogTests. def test_region_crud(self): self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) @@ -172,10 +194,10 @@ class TestTemplatedCatalog(unit.TestCase, test_backend.CatalogTests): def test_create_region_with_duplicate_id(self): self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) - def test_delete_region_404(self): + def test_delete_region_returns_not_found(self): self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) - def test_create_region_invalid_parent_region_404(self): + def test_create_region_invalid_parent_region_returns_not_found(self): self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) def test_avoid_creating_circular_references_in_regions_update(self): @@ -203,7 +225,7 @@ class TestTemplatedCatalog(unit.TestCase, test_backend.CatalogTests): def test_cache_layer_delete_service_with_endpoint(self): self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) - def test_delete_service_404(self): + def test_delete_service_returns_not_found(self): self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) def test_update_endpoint_nonexistent_service(self): @@ -215,10 +237,10 @@ class TestTemplatedCatalog(unit.TestCase, test_backend.CatalogTests): def test_update_endpoint_nonexistent_region(self): self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) - def test_get_endpoint_404(self): + def test_get_endpoint_returns_not_found(self): self.skipTest("Templated backend doesn't use IDs for endpoints.") - def test_delete_endpoint_404(self): + def test_delete_endpoint_returns_not_found(self): self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) def test_create_endpoint(self): @@ -228,11 +250,11 @@ class TestTemplatedCatalog(unit.TestCase, test_backend.CatalogTests): self.skipTest(BROKEN_WRITE_FUNCTIONALITY_MSG) def test_list_endpoints(self): - # NOTE(dstanek): a future commit will fix this functionality and - # this test - expected_ids = set() + expected_urls = set(['http://localhost:$(public_port)s/v2.0', + 'http://localhost:$(admin_port)s/v2.0', + 'http://localhost:8774/v1.1/$(tenant_id)s']) endpoints = self.catalog_api.list_endpoints() - self.assertEqual(expected_ids, set(e['id'] for e in endpoints)) + self.assertEqual(expected_urls, set(e['url'] for e in endpoints)) @unit.skip_if_cache_disabled('catalog') def test_invalidate_cache_when_updating_endpoint(self): diff --git a/keystone-moon/keystone/tests/unit/test_catalog.py b/keystone-moon/keystone/tests/unit/test_catalog.py index ada2de43..76e3055a 100644 --- a/keystone-moon/keystone/tests/unit/test_catalog.py +++ b/keystone-moon/keystone/tests/unit/test_catalog.py @@ -31,12 +31,9 @@ class V2CatalogTestCase(rest.RestfulTestCase): super(V2CatalogTestCase, self).setUp() self.useFixture(database.Database()) - self.service_id = uuid.uuid4().hex self.service = unit.new_service_ref() - self.service['id'] = self.service_id - self.catalog_api.create_service( - self.service_id, - self.service.copy()) + self.service_id = self.service['id'] + self.catalog_api.create_service(self.service_id, self.service) # TODO(termie): add an admin user to the fixtures and use that user # override the fixtures, for now @@ -53,13 +50,14 @@ class V2CatalogTestCase(rest.RestfulTestCase): """Applicable only to JSON.""" return r.result['access']['token']['id'] - def _endpoint_create(self, expected_status=200, service_id=SERVICE_FIXTURE, + def _endpoint_create(self, expected_status=http_client.OK, + service_id=SERVICE_FIXTURE, publicurl='http://localhost:8080', internalurl='http://localhost:8080', adminurl='http://localhost:8080'): if service_id is SERVICE_FIXTURE: service_id = self.service_id - # FIXME(dolph): expected status should actually be 201 Created + path = '/v2.0/endpoints' body = { 'endpoint': { @@ -77,40 +75,33 @@ class V2CatalogTestCase(rest.RestfulTestCase): return body, r def _region_create(self): - region_id = uuid.uuid4().hex - self.catalog_api.create_region({'id': region_id}) + region = unit.new_region_ref() + region_id = region['id'] + self.catalog_api.create_region(region) return region_id - def _service_create(self): - service_id = uuid.uuid4().hex - service = unit.new_service_ref() - service['id'] = service_id - self.catalog_api.create_service(service_id, service) - return service_id - def test_endpoint_create(self): req_body, response = self._endpoint_create() self.assertIn('endpoint', response.result) self.assertIn('id', response.result['endpoint']) for field, value in req_body['endpoint'].items(): - self.assertEqual(response.result['endpoint'][field], value) + self.assertEqual(value, response.result['endpoint'][field]) def test_pure_v3_endpoint_with_publicurl_visible_from_v2(self): - """Test pure v3 endpoint can be fetched via v2 API. + """Test pure v3 endpoint can be fetched via v2.0 API. - For those who are using v2 APIs, endpoints created by v3 API should + For those who are using v2.0 APIs, endpoints created by v3 API should also be visible as there are no differences about the endpoints - except the format or the internal implementation. - And because public url is required for v2 API, so only the v3 endpoints - of the service which has the public interface endpoint will be - converted into v2 endpoints. + except the format or the internal implementation. Since publicURL is + required for v2.0 API, so only v3 endpoints of the service which have + the public interface endpoint will be converted into v2.0 endpoints. """ region_id = self._region_create() - service_id = self._service_create() - # create a v3 endpoint with three interfaces + + # create v3 endpoints with three interfaces body = { - 'endpoint': unit.new_endpoint_ref(service_id, - default_region_id=region_id) + 'endpoint': unit.new_endpoint_ref(self.service_id, + region_id=region_id) } for interface in catalog.controllers.INTERFACES: body['endpoint']['interface'] = interface @@ -122,11 +113,11 @@ class V2CatalogTestCase(rest.RestfulTestCase): r = self.admin_request(token=self.get_scoped_token(), path='/v2.0/endpoints') - # v3 endpoints having public url can be fetched via v2.0 API + # Endpoints of the service which have a public interface endpoint + # will be returned via v2.0 API self.assertEqual(1, len(r.result['endpoints'])) v2_endpoint = r.result['endpoints'][0] - self.assertEqual(service_id, v2_endpoint['service_id']) - # check urls just in case. + self.assertEqual(self.service_id, v2_endpoint['service_id']) # This is not the focus of this test, so no different urls are used. self.assertEqual(body['endpoint']['url'], v2_endpoint['publicurl']) self.assertEqual(body['endpoint']['url'], v2_endpoint['adminurl']) @@ -134,23 +125,23 @@ class V2CatalogTestCase(rest.RestfulTestCase): self.assertNotIn('name', v2_endpoint) v3_endpoint = self.catalog_api.get_endpoint(v2_endpoint['id']) - # it's the v3 public endpoint's id as the generated v2 endpoint + # Checks the v3 public endpoint's id is the generated v2.0 endpoint self.assertEqual('public', v3_endpoint['interface']) - self.assertEqual(service_id, v3_endpoint['service_id']) + self.assertEqual(self.service_id, v3_endpoint['service_id']) def test_pure_v3_endpoint_without_publicurl_invisible_from_v2(self): - """Test pure v3 endpoint without public url can't be fetched via v2 API. + """Test that the v2.0 API can't fetch v3 endpoints without publicURLs. - V2 API will return endpoints created by v3 API, but because public url - is required for v2 API, so v3 endpoints without public url will be - ignored. + v2.0 API will return endpoints created by v3 API, but publicURL is + required for the service in the v2.0 API, therefore v3 endpoints of + a service which don't have publicURL will be ignored. """ region_id = self._region_create() - service_id = self._service_create() + # create a v3 endpoint without public interface body = { - 'endpoint': unit.new_endpoint_ref(service_id, - default_region_id=region_id) + 'endpoint': unit.new_endpoint_ref(self.service_id, + region_id=region_id) } for interface in catalog.controllers.INTERFACES: if interface == 'public': @@ -164,7 +155,8 @@ class V2CatalogTestCase(rest.RestfulTestCase): r = self.admin_request(token=self.get_scoped_token(), path='/v2.0/endpoints') - # v3 endpoints without public url won't be fetched via v2.0 API + # v3 endpoints of a service which don't have publicURL can't be + # fetched via v2.0 API self.assertEqual(0, len(r.result['endpoints'])) def test_endpoint_create_with_null_adminurl(self): @@ -209,7 +201,7 @@ class V2CatalogTestCase(rest.RestfulTestCase): valid_url = 'http://127.0.0.1:8774/v1.1/$(tenant_id)s' # baseline tests that all valid URLs works - self._endpoint_create(expected_status=200, + self._endpoint_create(expected_status=http_client.OK, publicurl=valid_url, internalurl=valid_url, adminurl=valid_url) @@ -297,28 +289,23 @@ class TestV2CatalogAPISQL(unit.TestCase): self.useFixture(database.Database()) self.catalog_api = catalog.Manager() - self.service_id = uuid.uuid4().hex - service = {'id': self.service_id, 'name': uuid.uuid4().hex} + service = unit.new_service_ref() + self.service_id = service['id'] self.catalog_api.create_service(self.service_id, service) - endpoint = self.new_endpoint_ref(service_id=self.service_id) + self.create_endpoint(service_id=self.service_id) + + def create_endpoint(self, service_id, **kwargs): + endpoint = unit.new_endpoint_ref(service_id=service_id, + region_id=None, + **kwargs) self.catalog_api.create_endpoint(endpoint['id'], endpoint) + return endpoint def config_overrides(self): super(TestV2CatalogAPISQL, self).config_overrides() self.config_fixture.config(group='catalog', driver='sql') - def new_endpoint_ref(self, service_id): - return { - 'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex, - 'description': uuid.uuid4().hex, - 'interface': uuid.uuid4().hex[:8], - 'service_id': service_id, - 'url': uuid.uuid4().hex, - 'region': uuid.uuid4().hex, - } - def test_get_catalog_ignores_endpoints_with_invalid_urls(self): user_id = uuid.uuid4().hex tenant_id = uuid.uuid4().hex @@ -330,14 +317,12 @@ class TestV2CatalogAPISQL(unit.TestCase): self.assertEqual(1, len(self.catalog_api.list_endpoints())) # create a new, invalid endpoint - malformed type declaration - endpoint = self.new_endpoint_ref(self.service_id) - endpoint['url'] = 'http://keystone/%(tenant_id)' - self.catalog_api.create_endpoint(endpoint['id'], endpoint) + self.create_endpoint(self.service_id, + url='http://keystone/%(tenant_id)') # create a new, invalid endpoint - nonexistent key - endpoint = self.new_endpoint_ref(self.service_id) - endpoint['url'] = 'http://keystone/%(you_wont_find_me)s' - self.catalog_api.create_endpoint(endpoint['id'], endpoint) + self.create_endpoint(self.service_id, + url='http://keystone/%(you_wont_find_me)s') # verify that the invalid endpoints don't appear in the catalog catalog = self.catalog_api.get_catalog(user_id, tenant_id) @@ -349,28 +334,22 @@ class TestV2CatalogAPISQL(unit.TestCase): user_id = uuid.uuid4().hex tenant_id = uuid.uuid4().hex - # create a service, with a name - named_svc = { - 'id': uuid.uuid4().hex, - 'type': uuid.uuid4().hex, - 'name': uuid.uuid4().hex, - } + # new_service_ref() returns a ref with a `name`. + named_svc = unit.new_service_ref() self.catalog_api.create_service(named_svc['id'], named_svc) - endpoint = self.new_endpoint_ref(service_id=named_svc['id']) - self.catalog_api.create_endpoint(endpoint['id'], endpoint) + self.create_endpoint(service_id=named_svc['id']) - # create a service, with no name - unnamed_svc = { - 'id': uuid.uuid4().hex, - 'type': uuid.uuid4().hex - } + # This time manually delete the generated `name`. + unnamed_svc = unit.new_service_ref() + del unnamed_svc['name'] self.catalog_api.create_service(unnamed_svc['id'], unnamed_svc) - endpoint = self.new_endpoint_ref(service_id=unnamed_svc['id']) - self.catalog_api.create_endpoint(endpoint['id'], endpoint) + self.create_endpoint(service_id=unnamed_svc['id']) region = None catalog = self.catalog_api.get_catalog(user_id, tenant_id) self.assertEqual(named_svc['name'], catalog[region][named_svc['type']]['name']) + + # verify a name is not generated when the service is passed to the API self.assertEqual('', catalog[region][unnamed_svc['type']]['name']) diff --git a/keystone-moon/keystone/tests/unit/test_cert_setup.py b/keystone-moon/keystone/tests/unit/test_cert_setup.py index 769e7c8e..debf87f5 100644 --- a/keystone-moon/keystone/tests/unit/test_cert_setup.py +++ b/keystone-moon/keystone/tests/unit/test_cert_setup.py @@ -17,6 +17,7 @@ import os import shutil import mock +from six.moves import http_client from testtools import matchers from keystone.common import environment @@ -29,7 +30,6 @@ from keystone import token SSLDIR = unit.dirs.tmp('ssl') CONF = unit.CONF -DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id CERTDIR = os.path.join(SSLDIR, 'certs') @@ -74,17 +74,12 @@ class CertSetupTestCase(rest.RestfulTestCase): controller = token.controllers.Auth() self.config_fixture.config(group='signing', certfile='invalid') - password = 'fake1' - user = { - 'name': 'fake1', - 'password': password, - 'domain_id': DEFAULT_DOMAIN_ID - } - user = self.identity_api.create_user(user) + user = unit.create_user(self.identity_api, + domain_id=CONF.identity.default_domain_id) body_dict = { 'passwordCredentials': { 'userId': user['id'], - 'password': password, + 'password': user['password'], }, } self.assertRaises(exception.UnexpectedError, @@ -113,11 +108,13 @@ class CertSetupTestCase(rest.RestfulTestCase): # requests don't have some of the normal information signing_resp = self.request(self.public_app, '/v2.0/certificates/signing', - method='GET', expected_status=200) + method='GET', + expected_status=http_client.OK) cacert_resp = self.request(self.public_app, '/v2.0/certificates/ca', - method='GET', expected_status=200) + method='GET', + expected_status=http_client.OK) with open(CONF.signing.certfile) as f: self.assertEqual(f.read(), signing_resp.text) @@ -133,7 +130,7 @@ class CertSetupTestCase(rest.RestfulTestCase): for accept in [None, 'text/html', 'application/json', 'text/xml']: headers = {'Accept': accept} if accept else {} resp = self.request(self.public_app, path, method='GET', - expected_status=200, + expected_status=http_client.OK, headers=headers) self.assertEqual('text/html', resp.content_type) @@ -146,7 +143,7 @@ class CertSetupTestCase(rest.RestfulTestCase): def test_failure(self): for path in ['/v2.0/certificates/signing', '/v2.0/certificates/ca']: self.request(self.public_app, path, method='GET', - expected_status=500) + expected_status=http_client.INTERNAL_SERVER_ERROR) def test_pki_certs_rebuild(self): self.test_create_pki_certs() @@ -228,15 +225,17 @@ class TestExecCommand(unit.TestCase): ssl = openssl.ConfigureSSL('keystone_user', 'keystone_group') ssl.exec_command(['ls']) - @mock.patch.object(environment.subprocess.Popen, 'communicate') - @mock.patch.object(environment.subprocess.Popen, 'poll') - def test_running_an_invalid_command(self, mock_poll, mock_communicate): + @mock.patch.object(environment.subprocess, 'check_output') + def test_running_an_invalid_command(self, mock_check_output): + cmd = ['ls'] + output = 'this is the output string' - mock_communicate.return_value = (output, '') - mock_poll.return_value = 1 + error = environment.subprocess.CalledProcessError(returncode=1, + cmd=cmd, + output=output) + mock_check_output.side_effect = error - cmd = ['ls'] ssl = openssl.ConfigureSSL('keystone_user', 'keystone_group') e = self.assertRaises(environment.subprocess.CalledProcessError, ssl.exec_command, diff --git a/keystone-moon/keystone/tests/unit/test_cli.py b/keystone-moon/keystone/tests/unit/test_cli.py index d967eb53..06f2e172 100644 --- a/keystone-moon/keystone/tests/unit/test_cli.py +++ b/keystone-moon/keystone/tests/unit/test_cli.py @@ -15,9 +15,11 @@ import os import uuid +import fixtures import mock from oslo_config import cfg from six.moves import range +from testtools import matchers from keystone.cmd import cli from keystone.common import dependency @@ -42,6 +44,199 @@ class CliTestCase(unit.SQLDriverOverrides, unit.TestCase): cli.TokenFlush.main() +class CliBootStrapTestCase(unit.SQLDriverOverrides, unit.TestCase): + + def setUp(self): + self.useFixture(database.Database()) + super(CliBootStrapTestCase, self).setUp() + + def config_files(self): + self.config_fixture.register_cli_opt(cli.command_opt) + config_files = super(CliBootStrapTestCase, self).config_files() + config_files.append(unit.dirs.tests_conf('backend_sql.conf')) + return config_files + + def config(self, config_files): + CONF(args=['bootstrap', '--bootstrap-password', uuid.uuid4().hex], + project='keystone', + default_config_files=config_files) + + def test_bootstrap(self): + bootstrap = cli.BootStrap() + self._do_test_bootstrap(bootstrap) + + def _do_test_bootstrap(self, bootstrap): + bootstrap.do_bootstrap() + project = bootstrap.resource_manager.get_project_by_name( + bootstrap.project_name, + 'default') + user = bootstrap.identity_manager.get_user_by_name( + bootstrap.username, + 'default') + role = bootstrap.role_manager.get_role(bootstrap.role_id) + role_list = ( + bootstrap.assignment_manager.get_roles_for_user_and_project( + user['id'], + project['id'])) + self.assertIs(len(role_list), 1) + self.assertEqual(role_list[0], role['id']) + # NOTE(morganfainberg): Pass an empty context, it isn't used by + # `authenticate` method. + bootstrap.identity_manager.authenticate( + {}, + user['id'], + bootstrap.password) + + if bootstrap.region_id: + region = bootstrap.catalog_manager.get_region(bootstrap.region_id) + self.assertEqual(self.region_id, region['id']) + + if bootstrap.service_id: + svc = bootstrap.catalog_manager.get_service(bootstrap.service_id) + self.assertEqual(self.service_name, svc['name']) + + self.assertEqual(set(['admin', 'public', 'internal']), + set(bootstrap.endpoints)) + + urls = {'public': self.public_url, + 'internal': self.internal_url, + 'admin': self.admin_url} + + for interface, url in urls.items(): + endpoint_id = bootstrap.endpoints[interface] + endpoint = bootstrap.catalog_manager.get_endpoint(endpoint_id) + + self.assertEqual(self.region_id, endpoint['region_id']) + self.assertEqual(url, endpoint['url']) + self.assertEqual(svc['id'], endpoint['service_id']) + self.assertEqual(interface, endpoint['interface']) + + def test_bootstrap_is_idempotent(self): + # NOTE(morganfainberg): Ensure we can run bootstrap multiple times + # without erroring. + bootstrap = cli.BootStrap() + self._do_test_bootstrap(bootstrap) + self._do_test_bootstrap(bootstrap) + + +class CliBootStrapTestCaseWithEnvironment(CliBootStrapTestCase): + + def config(self, config_files): + CONF(args=['bootstrap'], project='keystone', + default_config_files=config_files) + + def setUp(self): + super(CliBootStrapTestCaseWithEnvironment, self).setUp() + self.password = uuid.uuid4().hex + self.username = uuid.uuid4().hex + self.project_name = uuid.uuid4().hex + self.role_name = uuid.uuid4().hex + self.service_name = uuid.uuid4().hex + self.public_url = uuid.uuid4().hex + self.internal_url = uuid.uuid4().hex + self.admin_url = uuid.uuid4().hex + self.region_id = uuid.uuid4().hex + self.default_domain = { + 'id': CONF.identity.default_domain_id, + 'name': 'Default', + } + self.useFixture( + fixtures.EnvironmentVariable('OS_BOOTSTRAP_PASSWORD', + newvalue=self.password)) + self.useFixture( + fixtures.EnvironmentVariable('OS_BOOTSTRAP_USERNAME', + newvalue=self.username)) + self.useFixture( + fixtures.EnvironmentVariable('OS_BOOTSTRAP_PROJECT_NAME', + newvalue=self.project_name)) + self.useFixture( + fixtures.EnvironmentVariable('OS_BOOTSTRAP_ROLE_NAME', + newvalue=self.role_name)) + self.useFixture( + fixtures.EnvironmentVariable('OS_BOOTSTRAP_SERVICE_NAME', + newvalue=self.service_name)) + self.useFixture( + fixtures.EnvironmentVariable('OS_BOOTSTRAP_PUBLIC_URL', + newvalue=self.public_url)) + self.useFixture( + fixtures.EnvironmentVariable('OS_BOOTSTRAP_INTERNAL_URL', + newvalue=self.internal_url)) + self.useFixture( + fixtures.EnvironmentVariable('OS_BOOTSTRAP_ADMIN_URL', + newvalue=self.admin_url)) + self.useFixture( + fixtures.EnvironmentVariable('OS_BOOTSTRAP_REGION_ID', + newvalue=self.region_id)) + + def test_assignment_created_with_user_exists(self): + # test assignment can be created if user already exists. + bootstrap = cli.BootStrap() + bootstrap.resource_manager.create_domain(self.default_domain['id'], + self.default_domain) + user_ref = unit.new_user_ref(self.default_domain['id'], + name=self.username, + password=self.password) + bootstrap.identity_manager.create_user(user_ref) + self._do_test_bootstrap(bootstrap) + + def test_assignment_created_with_project_exists(self): + # test assignment can be created if project already exists. + bootstrap = cli.BootStrap() + bootstrap.resource_manager.create_domain(self.default_domain['id'], + self.default_domain) + project_ref = unit.new_project_ref(self.default_domain['id'], + name=self.project_name) + bootstrap.resource_manager.create_project(project_ref['id'], + project_ref) + self._do_test_bootstrap(bootstrap) + + def test_assignment_created_with_role_exists(self): + # test assignment can be created if role already exists. + bootstrap = cli.BootStrap() + bootstrap.resource_manager.create_domain(self.default_domain['id'], + self.default_domain) + role = unit.new_role_ref(name=self.role_name) + bootstrap.role_manager.create_role(role['id'], role) + self._do_test_bootstrap(bootstrap) + + def test_assignment_created_with_region_exists(self): + # test assignment can be created if role already exists. + bootstrap = cli.BootStrap() + bootstrap.resource_manager.create_domain(self.default_domain['id'], + self.default_domain) + region = unit.new_region_ref(id=self.region_id) + bootstrap.catalog_manager.create_region(region) + self._do_test_bootstrap(bootstrap) + + def test_endpoints_created_with_service_exists(self): + # test assignment can be created if role already exists. + bootstrap = cli.BootStrap() + bootstrap.resource_manager.create_domain(self.default_domain['id'], + self.default_domain) + service = unit.new_service_ref(name=self.service_name) + bootstrap.catalog_manager.create_service(service['id'], service) + self._do_test_bootstrap(bootstrap) + + def test_endpoints_created_with_endpoint_exists(self): + # test assignment can be created if role already exists. + bootstrap = cli.BootStrap() + bootstrap.resource_manager.create_domain(self.default_domain['id'], + self.default_domain) + service = unit.new_service_ref(name=self.service_name) + bootstrap.catalog_manager.create_service(service['id'], service) + + region = unit.new_region_ref(id=self.region_id) + bootstrap.catalog_manager.create_region(region) + + endpoint = unit.new_endpoint_ref(interface='public', + service_id=service['id'], + url=self.public_url, + region_id=self.region_id) + bootstrap.catalog_manager.create_endpoint(endpoint['id'], endpoint) + + self._do_test_bootstrap(bootstrap) + + class CliDomainConfigAllTestCase(unit.SQLDriverOverrides, unit.TestCase): def setUp(self): @@ -112,7 +307,8 @@ class CliDomainConfigAllTestCase(unit.SQLDriverOverrides, unit.TestCase): 'user': 'cn=Admin', 'password': 'password', 'suffix': 'cn=example,cn=com'}, - 'identity': {'driver': 'ldap'} + 'identity': {'driver': 'ldap', + 'list_limit': '101'} } domain2_config = { 'ldap': {'url': 'fake://memory', @@ -182,8 +378,8 @@ class CliDomainConfigSingleDomainTestCase(CliDomainConfigAllTestCase): # Now try and upload the settings in the configuration file for the # default domain dependency.reset() - with mock.patch('__builtin__.print') as mock_print: - self.assertRaises(SystemExit, cli.DomainConfigUpload.main) + with mock.patch('six.moves.builtins.print') as mock_print: + self.assertRaises(unit.UnexpectedExit, cli.DomainConfigUpload.main) file_name = ('keystone.%s.conf' % resource.calc_default_domain()['name']) error_msg = _( @@ -208,8 +404,8 @@ class CliDomainConfigNoOptionsTestCase(CliDomainConfigAllTestCase): def test_config_upload(self): dependency.reset() - with mock.patch('__builtin__.print') as mock_print: - self.assertRaises(SystemExit, cli.DomainConfigUpload.main) + with mock.patch('six.moves.builtins.print') as mock_print: + self.assertRaises(unit.UnexpectedExit, cli.DomainConfigUpload.main) mock_print.assert_has_calls( [mock.call( _('At least one option must be provided, use either ' @@ -225,8 +421,8 @@ class CliDomainConfigTooManyOptionsTestCase(CliDomainConfigAllTestCase): def test_config_upload(self): dependency.reset() - with mock.patch('__builtin__.print') as mock_print: - self.assertRaises(SystemExit, cli.DomainConfigUpload.main) + with mock.patch('six.moves.builtins.print') as mock_print: + self.assertRaises(unit.UnexpectedExit, cli.DomainConfigUpload.main) mock_print.assert_has_calls( [mock.call(_('The --all option cannot be used with ' 'the --domain-name option'))]) @@ -242,8 +438,8 @@ class CliDomainConfigInvalidDomainTestCase(CliDomainConfigAllTestCase): def test_config_upload(self): dependency.reset() - with mock.patch('__builtin__.print') as mock_print: - self.assertRaises(SystemExit, cli.DomainConfigUpload.main) + with mock.patch('six.moves.builtins.print') as mock_print: + self.assertRaises(unit.UnexpectedExit, cli.DomainConfigUpload.main) file_name = 'keystone.%s.conf' % self.invalid_domain_name error_msg = (_( 'Invalid domain name: %(domain)s found in config file name: ' @@ -252,3 +448,31 @@ class CliDomainConfigInvalidDomainTestCase(CliDomainConfigAllTestCase): 'file': os.path.join(CONF.identity.domain_config_dir, file_name)}) mock_print.assert_has_calls([mock.call(error_msg)]) + + +class TestDomainConfigFinder(unit.BaseTestCase): + + def setUp(self): + super(TestDomainConfigFinder, self).setUp() + self.logging = self.useFixture(fixtures.LoggerFixture()) + + @mock.patch('os.walk') + def test_finder_ignores_files(self, mock_walk): + mock_walk.return_value = [ + ['.', [], ['file.txt', 'keystone.conf', 'keystone.domain0.conf']], + ] + + domain_configs = list(cli._domain_config_finder('.')) + + expected_domain_configs = [('./keystone.domain0.conf', 'domain0')] + self.assertThat(domain_configs, + matchers.Equals(expected_domain_configs)) + + expected_msg_template = ('Ignoring file (%s) while scanning ' + 'domain config directory') + self.assertThat( + self.logging.output, + matchers.Contains(expected_msg_template % 'file.txt')) + self.assertThat( + self.logging.output, + matchers.Contains(expected_msg_template % 'keystone.conf')) diff --git a/keystone-moon/keystone/tests/unit/test_config.py b/keystone-moon/keystone/tests/unit/test_config.py index 7984646d..d7e7809f 100644 --- a/keystone-moon/keystone/tests/unit/test_config.py +++ b/keystone-moon/keystone/tests/unit/test_config.py @@ -16,7 +16,7 @@ import uuid from oslo_config import cfg -from keystone import config +from keystone.common import config from keystone import exception from keystone.tests import unit diff --git a/keystone-moon/keystone/tests/unit/test_contrib_s3_core.py b/keystone-moon/keystone/tests/unit/test_contrib_s3_core.py index 18c76dad..c9706da7 100644 --- a/keystone-moon/keystone/tests/unit/test_contrib_s3_core.py +++ b/keystone-moon/keystone/tests/unit/test_contrib_s3_core.py @@ -27,9 +27,9 @@ class S3ContribCore(unit.TestCase): self.controller = s3.S3Controller() - def test_good_signature(self): + def test_good_signature_v1(self): creds_ref = {'secret': - 'b121dd41cdcc42fe9f70e572e84295aa'} + u'b121dd41cdcc42fe9f70e572e84295aa'} credentials = {'token': 'UFVUCjFCMk0yWThBc2dUcGdBbVk3UGhDZmc9PQphcHB' 'saWNhdGlvbi9vY3RldC1zdHJlYW0KVHVlLCAxMSBEZWMgMjAxM' @@ -40,9 +40,9 @@ class S3ContribCore(unit.TestCase): self.assertIsNone(self.controller.check_signature(creds_ref, credentials)) - def test_bad_signature(self): + def test_bad_signature_v1(self): creds_ref = {'secret': - 'b121dd41cdcc42fe9f70e572e84295aa'} + u'b121dd41cdcc42fe9f70e572e84295aa'} credentials = {'token': 'UFVUCjFCMk0yWThBc2dUcGdBbVk3UGhDZmc9PQphcHB' 'saWNhdGlvbi9vY3RldC1zdHJlYW0KVHVlLCAxMSBEZWMgMjAxM' @@ -53,3 +53,51 @@ class S3ContribCore(unit.TestCase): self.assertRaises(exception.Unauthorized, self.controller.check_signature, creds_ref, credentials) + + def test_good_signature_v4(self): + creds_ref = {'secret': + u'e7a7a2240136494986991a6598d9fb9f'} + credentials = {'token': + 'QVdTNC1ITUFDLVNIQTI1NgoyMDE1MDgyNFQxMTIwNDFaCjIw' + 'MTUwODI0L1JlZ2lvbk9uZS9zMy9hd3M0X3JlcXVlc3QKZjIy' + 'MTU1ODBlZWI5YTE2NzM1MWJkOTNlODZjM2I2ZjA0YTkyOGY1' + 'YzU1MjBhMzkzNWE0NTM1NDBhMDk1NjRiNQ==', + 'signature': + '730ba8f58df6ffeadd78f402e990b2910d60' + 'bc5c2aec63619734f096a4dd77be'} + + self.assertIsNone(self.controller.check_signature(creds_ref, + credentials)) + + def test_bad_signature_v4(self): + creds_ref = {'secret': + u'e7a7a2240136494986991a6598d9fb9f'} + credentials = {'token': + 'QVdTNC1ITUFDLVNIQTI1NgoyMDE1MDgyNFQxMTIwNDFaCjIw' + 'MTUwODI0L1JlZ2lvbk9uZS9zMy9hd3M0X3JlcXVlc3QKZjIy' + 'MTU1ODBlZWI5YTE2NzM1MWJkOTNlODZjM2I2ZjA0YTkyOGY1' + 'YzU1MjBhMzkzNWE0NTM1NDBhMDk1NjRiNQ==', + 'signature': uuid.uuid4().hex} + + self.assertRaises(exception.Unauthorized, + self.controller.check_signature, + creds_ref, credentials) + + def test_bad_token_v4(self): + creds_ref = {'secret': + u'e7a7a2240136494986991a6598d9fb9f'} + # token has invalid format of first part + credentials = {'token': + 'QVdTNC1BQUEKWApYClg=', + 'signature': ''} + self.assertRaises(exception.Unauthorized, + self.controller.check_signature, + creds_ref, credentials) + + # token has invalid format of scope + credentials = {'token': + 'QVdTNC1ITUFDLVNIQTI1NgpYCi8vczMvYXdzTl9yZXF1ZXN0Clg=', + 'signature': ''} + self.assertRaises(exception.Unauthorized, + self.controller.check_signature, + creds_ref, credentials) diff --git a/keystone-moon/keystone/tests/unit/test_contrib_simple_cert.py b/keystone-moon/keystone/tests/unit/test_contrib_simple_cert.py index 8664e2c3..111aa5c6 100644 --- a/keystone-moon/keystone/tests/unit/test_contrib_simple_cert.py +++ b/keystone-moon/keystone/tests/unit/test_contrib_simple_cert.py @@ -12,13 +12,13 @@ import uuid +from six.moves import http_client + from keystone.tests.unit import test_v3 class BaseTestCase(test_v3.RestfulTestCase): - EXTENSION_TO_ADD = 'simple_cert_extension' - CA_PATH = '/v3/OS-SIMPLE-CERT/ca' CERT_PATH = '/v3/OS-SIMPLE-CERT/certificates' @@ -31,10 +31,10 @@ class TestSimpleCert(BaseTestCase): method='GET', path=path, headers={'Accept': content_type}, - expected_status=200) + expected_status=http_client.OK) self.assertEqual(content_type, response.content_type.lower()) - self.assertIn('---BEGIN', response.body) + self.assertIn(b'---BEGIN', response.body) return response @@ -54,4 +54,4 @@ class TestSimpleCert(BaseTestCase): self.request(app=self.public_app, method='GET', path=path, - expected_status=500) + expected_status=http_client.INTERNAL_SERVER_ERROR) diff --git a/keystone-moon/keystone/tests/unit/test_credential.py b/keystone-moon/keystone/tests/unit/test_credential.py new file mode 100644 index 00000000..e917ef71 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_credential.py @@ -0,0 +1,265 @@ +# Copyright 2015 UnitedStack, Inc +# +# 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 keystoneclient.contrib.ec2 import utils as ec2_utils +from six.moves import http_client + +from keystone.common import utils +from keystone.contrib.ec2 import controllers +from keystone import exception +from keystone.tests import unit +from keystone.tests.unit import default_fixtures +from keystone.tests.unit.ksfixtures import database +from keystone.tests.unit import rest + +CRED_TYPE_EC2 = controllers.CRED_TYPE_EC2 + + +class V2CredentialEc2TestCase(rest.RestfulTestCase): + def setUp(self): + super(V2CredentialEc2TestCase, self).setUp() + self.user_id = self.user_foo['id'] + self.project_id = self.tenant_bar['id'] + + def _get_token_id(self, r): + return r.result['access']['token']['id'] + + def _get_ec2_cred(self): + uri = self._get_ec2_cred_uri() + r = self.public_request(method='POST', token=self.get_scoped_token(), + path=uri, body={'tenant_id': self.project_id}) + return r.result['credential'] + + def _get_ec2_cred_uri(self): + return '/v2.0/users/%s/credentials/OS-EC2' % self.user_id + + def test_ec2_cannot_get_non_ec2_credential(self): + access_key = uuid.uuid4().hex + cred_id = utils.hash_access_key(access_key) + non_ec2_cred = unit.new_credential_ref( + user_id=self.user_id, + project_id=self.project_id) + non_ec2_cred['id'] = cred_id + self.credential_api.create_credential(cred_id, non_ec2_cred) + + # if access_key is not found, ec2 controller raises Unauthorized + # exception + path = '/'.join([self._get_ec2_cred_uri(), access_key]) + self.public_request(method='GET', token=self.get_scoped_token(), + path=path, + expected_status=http_client.UNAUTHORIZED) + + def assertValidErrorResponse(self, r): + # FIXME(wwwjfy): it's copied from test_v3.py. The logic of this method + # in test_v2.py and test_v3.py (both are inherited from rest.py) has no + # difference, so they should be refactored into one place. Also, the + # function signatures in both files don't match the one in the parent + # class in rest.py. + resp = r.result + self.assertIsNotNone(resp.get('error')) + self.assertIsNotNone(resp['error'].get('code')) + self.assertIsNotNone(resp['error'].get('title')) + self.assertIsNotNone(resp['error'].get('message')) + self.assertEqual(int(resp['error']['code']), r.status_code) + + def test_ec2_list_credentials(self): + self._get_ec2_cred() + uri = self._get_ec2_cred_uri() + r = self.public_request(method='GET', token=self.get_scoped_token(), + path=uri) + cred_list = r.result['credentials'] + self.assertEqual(1, len(cred_list)) + + # non-EC2 credentials won't be fetched + non_ec2_cred = unit.new_credential_ref( + user_id=self.user_id, + project_id=self.project_id) + non_ec2_cred['type'] = uuid.uuid4().hex + self.credential_api.create_credential(non_ec2_cred['id'], + non_ec2_cred) + r = self.public_request(method='GET', token=self.get_scoped_token(), + path=uri) + cred_list_2 = r.result['credentials'] + # still one element because non-EC2 credentials are not returned. + self.assertEqual(1, len(cred_list_2)) + self.assertEqual(cred_list[0], cred_list_2[0]) + + +class V2CredentialEc2Controller(unit.TestCase): + def setUp(self): + super(V2CredentialEc2Controller, self).setUp() + self.useFixture(database.Database()) + self.load_backends() + self.load_fixtures(default_fixtures) + self.user_id = self.user_foo['id'] + self.project_id = self.tenant_bar['id'] + self.controller = controllers.Ec2Controller() + self.blob, tmp_ref = unit.new_ec2_credential( + user_id=self.user_id, + project_id=self.project_id) + + self.creds_ref = (controllers.Ec2Controller + ._convert_v3_to_ec2_credential(tmp_ref)) + + def test_signature_validate_no_host_port(self): + """Test signature validation with the access/secret provided.""" + access = self.blob['access'] + secret = self.blob['secret'] + signer = ec2_utils.Ec2Signer(secret) + params = {'SignatureMethod': 'HmacSHA256', + 'SignatureVersion': '2', + 'AWSAccessKeyId': access} + request = {'host': 'foo', + 'verb': 'GET', + 'path': '/bar', + 'params': params} + signature = signer.generate(request) + + sig_ref = {'access': access, + 'signature': signature, + 'host': 'foo', + 'verb': 'GET', + 'path': '/bar', + 'params': params} + + # Now validate the signature based on the dummy request + self.assertTrue(self.controller.check_signature(self.creds_ref, + sig_ref)) + + def test_signature_validate_with_host_port(self): + """Test signature validation when host is bound with port. + + Host is bound with a port, generally, the port here is not the + standard port for the protocol, like '80' for HTTP and port 443 + for HTTPS, the port is not omitted by the client library. + """ + access = self.blob['access'] + secret = self.blob['secret'] + signer = ec2_utils.Ec2Signer(secret) + params = {'SignatureMethod': 'HmacSHA256', + 'SignatureVersion': '2', + 'AWSAccessKeyId': access} + request = {'host': 'foo:8181', + 'verb': 'GET', + 'path': '/bar', + 'params': params} + signature = signer.generate(request) + + sig_ref = {'access': access, + 'signature': signature, + 'host': 'foo:8181', + 'verb': 'GET', + 'path': '/bar', + 'params': params} + + # Now validate the signature based on the dummy request + self.assertTrue(self.controller.check_signature(self.creds_ref, + sig_ref)) + + def test_signature_validate_with_missed_host_port(self): + """Test signature validation when host is bound with well-known port. + + Host is bound with a port, but the port is well-know port like '80' + for HTTP and port 443 for HTTPS, sometimes, client library omit + the port but then make the request with the port. + see (How to create the string to sign): 'http://docs.aws.amazon.com/ + general/latest/gr/signature-version-2.html'. + + Since "credentials['host']" is not set by client library but is + taken from "req.host", so caused the differences. + """ + access = self.blob['access'] + secret = self.blob['secret'] + signer = ec2_utils.Ec2Signer(secret) + params = {'SignatureMethod': 'HmacSHA256', + 'SignatureVersion': '2', + 'AWSAccessKeyId': access} + # Omit the port to generate the signature. + cnt_req = {'host': 'foo', + 'verb': 'GET', + 'path': '/bar', + 'params': params} + signature = signer.generate(cnt_req) + + sig_ref = {'access': access, + 'signature': signature, + 'host': 'foo:8080', + 'verb': 'GET', + 'path': '/bar', + 'params': params} + + # Now validate the signature based on the dummy request + # Check the signature again after omitting the port. + self.assertTrue(self.controller.check_signature(self.creds_ref, + sig_ref)) + + def test_signature_validate_no_signature(self): + """Signature is not presented in signature reference data.""" + access = self.blob['access'] + params = {'SignatureMethod': 'HmacSHA256', + 'SignatureVersion': '2', + 'AWSAccessKeyId': access} + + sig_ref = {'access': access, + 'signature': None, + 'host': 'foo:8080', + 'verb': 'GET', + 'path': '/bar', + 'params': params} + + # Now validate the signature based on the dummy request + self.assertRaises(exception.Unauthorized, + self.controller.check_signature, + self.creds_ref, sig_ref) + + def test_signature_validate_invalid_signature(self): + """Signature is not signed on the correct data.""" + access = self.blob['access'] + secret = self.blob['secret'] + signer = ec2_utils.Ec2Signer(secret) + params = {'SignatureMethod': 'HmacSHA256', + 'SignatureVersion': '2', + 'AWSAccessKeyId': access} + request = {'host': 'bar', + 'verb': 'GET', + 'path': '/bar', + 'params': params} + signature = signer.generate(request) + + sig_ref = {'access': access, + 'signature': signature, + 'host': 'foo:8080', + 'verb': 'GET', + 'path': '/bar', + 'params': params} + + # Now validate the signature based on the dummy request + self.assertRaises(exception.Unauthorized, + self.controller.check_signature, + self.creds_ref, sig_ref) + + def test_check_non_admin_user(self): + """Checking if user is admin causes uncaught error. + + When checking if a user is an admin, keystone.exception.Unauthorized + is raised but not caught if the user is not an admin. + """ + # make a non-admin user + context = {'is_admin': False, 'token_id': uuid.uuid4().hex} + + # check if user is admin + # no exceptions should be raised + self.controller._is_admin(context) diff --git a/keystone-moon/keystone/tests/unit/test_driver_hints.py b/keystone-moon/keystone/tests/unit/test_driver_hints.py index c20d2ae7..75d76194 100644 --- a/keystone-moon/keystone/tests/unit/test_driver_hints.py +++ b/keystone-moon/keystone/tests/unit/test_driver_hints.py @@ -27,7 +27,7 @@ class ListHintsTests(test.TestCase): self.assertEqual('t1', filter['name']) self.assertEqual('data1', filter['value']) self.assertEqual('equals', filter['comparator']) - self.assertEqual(False, filter['case_sensitive']) + self.assertFalse(filter['case_sensitive']) hints.filters.remove(filter) filter_count = 0 diff --git a/keystone-moon/keystone/tests/unit/test_entry_points.py b/keystone-moon/keystone/tests/unit/test_entry_points.py new file mode 100644 index 00000000..e973e942 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_entry_points.py @@ -0,0 +1,48 @@ +# 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 stevedore +from testtools import matchers + +from keystone.tests.unit import core as test + + +class TestPasteDeploymentEntryPoints(test.TestCase): + def test_entry_point_middleware(self): + """Assert that our list of expected middleware is present.""" + expected_names = [ + 'admin_token_auth', + 'build_auth_context', + 'crud_extension', + 'cors', + 'debug', + 'endpoint_filter_extension', + 'ec2_extension', + 'ec2_extension_v3', + 'federation_extension', + 'json_body', + 'oauth1_extension', + 'request_id', + 'revoke_extension', + 's3_extension', + 'simple_cert_extension', + 'sizelimit', + 'token_auth', + 'url_normalize', + 'user_crud_extension', + ] + + em = stevedore.ExtensionManager('paste.filter_factory') + + actual_names = [extension.name for extension in em] + + self.assertThat(actual_names, matchers.ContainsAll(expected_names)) diff --git a/keystone-moon/keystone/tests/unit/test_exception.py b/keystone-moon/keystone/tests/unit/test_exception.py index 4d602ccc..25ca2c09 100644 --- a/keystone-moon/keystone/tests/unit/test_exception.py +++ b/keystone-moon/keystone/tests/unit/test_exception.py @@ -67,7 +67,7 @@ class ExceptionTestCase(unit.BaseTestCase): self.assertValidJsonRendering(e) self.assertIn(target, six.text_type(e)) - def test_403_title(self): + def test_forbidden_title(self): e = exception.Forbidden() resp = wsgi.render_exception(e) j = jsonutils.loads(resp.body) @@ -123,7 +123,7 @@ class UnexpectedExceptionTestCase(ExceptionTestCase): self.assertNotIn(self.exc_str, six.text_type(e)) def test_unexpected_error_debug(self): - self.config_fixture.config(debug=True) + self.config_fixture.config(debug=True, insecure_debug=True) e = exception.UnexpectedError(exception=self.exc_str) self.assertIn(self.exc_str, six.text_type(e)) @@ -131,32 +131,48 @@ class UnexpectedExceptionTestCase(ExceptionTestCase): self.config_fixture.config(debug=False) e = UnexpectedExceptionTestCase.SubClassExc( debug_info=self.exc_str) - self.assertEqual(exception.UnexpectedError._message_format, + self.assertEqual(exception.UnexpectedError.message_format, six.text_type(e)) def test_unexpected_error_subclass_debug(self): - self.config_fixture.config(debug=True) + self.config_fixture.config(debug=True, insecure_debug=True) subclass = self.SubClassExc e = subclass(debug_info=self.exc_str) expected = subclass.debug_message_format % {'debug_info': self.exc_str} - translated_amendment = six.text_type(exception.SecurityError.amendment) self.assertEqual( - expected + six.text_type(' ') + translated_amendment, + '%s %s' % (expected, exception.SecurityError.amendment), six.text_type(e)) def test_unexpected_error_custom_message_no_debug(self): self.config_fixture.config(debug=False) e = exception.UnexpectedError(self.exc_str) - self.assertEqual(exception.UnexpectedError._message_format, + self.assertEqual(exception.UnexpectedError.message_format, six.text_type(e)) def test_unexpected_error_custom_message_debug(self): - self.config_fixture.config(debug=True) + self.config_fixture.config(debug=True, insecure_debug=True) e = exception.UnexpectedError(self.exc_str) - translated_amendment = six.text_type(exception.SecurityError.amendment) self.assertEqual( - self.exc_str + six.text_type(' ') + translated_amendment, + '%s %s' % (self.exc_str, exception.SecurityError.amendment), + six.text_type(e)) + + def test_unexpected_error_custom_message_exception_debug(self): + self.config_fixture.config(debug=True, insecure_debug=True) + orig_e = exception.NotFound(target=uuid.uuid4().hex) + e = exception.UnexpectedError(orig_e) + self.assertEqual( + '%s %s' % (six.text_type(orig_e), + exception.SecurityError.amendment), + six.text_type(e)) + + def test_unexpected_error_custom_message_binary_debug(self): + self.config_fixture.config(debug=True, insecure_debug=True) + binary_msg = b'something' + e = exception.UnexpectedError(binary_msg) + self.assertEqual( + '%s %s' % (six.text_type(binary_msg), + exception.SecurityError.amendment), six.text_type(e)) @@ -176,7 +192,7 @@ class SecurityErrorTestCase(ExceptionTestCase): self.assertNotIn(risky_info, six.text_type(e)) def test_unauthorized_exposure_in_debug(self): - self.config_fixture.config(debug=True) + self.config_fixture.config(debug=True, insecure_debug=True) risky_info = uuid.uuid4().hex e = exception.Unauthorized(message=risky_info) @@ -192,7 +208,7 @@ class SecurityErrorTestCase(ExceptionTestCase): self.assertNotIn(risky_info, six.text_type(e)) def test_forbidden_exposure_in_debug(self): - self.config_fixture.config(debug=True) + self.config_fixture.config(debug=True, insecure_debug=True) risky_info = uuid.uuid4().hex e = exception.Forbidden(message=risky_info) @@ -208,23 +224,45 @@ class SecurityErrorTestCase(ExceptionTestCase): self.assertValidJsonRendering(e) self.assertNotIn(risky_info, six.text_type(e)) self.assertIn(action, six.text_type(e)) + self.assertNotIn(exception.SecurityError.amendment, six.text_type(e)) - e = exception.ForbiddenAction(action=risky_info) + e = exception.ForbiddenAction(action=action) self.assertValidJsonRendering(e) - self.assertIn(risky_info, six.text_type(e)) + self.assertIn(action, six.text_type(e)) + self.assertNotIn(exception.SecurityError.amendment, six.text_type(e)) def test_forbidden_action_exposure_in_debug(self): - self.config_fixture.config(debug=True) + self.config_fixture.config(debug=True, insecure_debug=True) risky_info = uuid.uuid4().hex + action = uuid.uuid4().hex - e = exception.ForbiddenAction(message=risky_info) + e = exception.ForbiddenAction(message=risky_info, action=action) self.assertValidJsonRendering(e) self.assertIn(risky_info, six.text_type(e)) + self.assertIn(exception.SecurityError.amendment, six.text_type(e)) - e = exception.ForbiddenAction(action=risky_info) + e = exception.ForbiddenAction(action=action) self.assertValidJsonRendering(e) - self.assertIn(risky_info, six.text_type(e)) + self.assertIn(action, six.text_type(e)) + self.assertNotIn(exception.SecurityError.amendment, six.text_type(e)) + + def test_forbidden_action_no_message(self): + # When no custom message is given when the ForbiddenAction (or other + # SecurityError subclass) is created the exposed message is the same + # whether debug is enabled or not. + + action = uuid.uuid4().hex + + self.config_fixture.config(debug=False) + e = exception.ForbiddenAction(action=action) + exposed_message = six.text_type(e) + self.assertIn(action, exposed_message) + self.assertNotIn(exception.SecurityError.amendment, six.text_type(e)) + + self.config_fixture.config(debug=True) + e = exception.ForbiddenAction(action=action) + self.assertEqual(exposed_message, six.text_type(e)) def test_unicode_argument_message(self): self.config_fixture.config(debug=False) diff --git a/keystone-moon/keystone/tests/unit/test_hacking_checks.py b/keystone-moon/keystone/tests/unit/test_hacking_checks.py index 962f5f8a..e279cc7f 100644 --- a/keystone-moon/keystone/tests/unit/test_hacking_checks.py +++ b/keystone-moon/keystone/tests/unit/test_hacking_checks.py @@ -86,25 +86,44 @@ class TestAssertingNoneEquality(BaseStyleCheck): self.assert_has_errors(code, expected_errors=errors) -class TestCheckForDebugLoggingIssues(BaseStyleCheck): +class BaseLoggingCheck(BaseStyleCheck): def get_checker(self): return checks.CheckForLoggingIssues + def get_fixture(self): + return hacking_fixtures.HackingLogging() + + def assert_has_errors(self, code, expected_errors=None): + + # pull out the parts of the error that we'll match against + actual_errors = (e[:3] for e in self.run_check(code)) + # adjust line numbers to make the fixture data more readable. + import_lines = len(self.code_ex.shared_imports.split('\n')) - 1 + actual_errors = [(e[0] - import_lines, e[1], e[2]) + for e in actual_errors] + self.assertEqual(expected_errors or [], actual_errors) + + +class TestCheckForDebugLoggingIssues(BaseLoggingCheck): + def test_for_translations(self): fixture = self.code_ex.assert_no_translations_for_debug_logging - code = fixture['code'] + code = self.code_ex.shared_imports + fixture['code'] errors = fixture['expected_errors'] self.assert_has_errors(code, expected_errors=errors) -class TestCheckForNonDebugLoggingIssues(BaseStyleCheck): +class TestLoggingWithWarn(BaseLoggingCheck): - def get_checker(self): - return checks.CheckForLoggingIssues + def test(self): + data = self.code_ex.assert_not_using_deprecated_warn + code = self.code_ex.shared_imports + data['code'] + errors = data['expected_errors'] + self.assert_has_errors(code, expected_errors=errors) - def get_fixture(self): - return hacking_fixtures.HackingLogging() + +class TestCheckForNonDebugLoggingIssues(BaseLoggingCheck): def test_for_translations(self): for example in self.code_ex.examples: @@ -112,15 +131,6 @@ class TestCheckForNonDebugLoggingIssues(BaseStyleCheck): errors = example['expected_errors'] self.assert_has_errors(code, expected_errors=errors) - def assert_has_errors(self, code, expected_errors=None): - # pull out the parts of the error that we'll match against - actual_errors = (e[:3] for e in self.run_check(code)) - # adjust line numbers to make the fixure data more readable. - import_lines = len(self.code_ex.shared_imports.split('\n')) - 1 - actual_errors = [(e[0] - import_lines, e[1], e[2]) - for e in actual_errors] - self.assertEqual(expected_errors or [], actual_errors) - class TestDictConstructorWithSequenceCopy(BaseStyleCheck): diff --git a/keystone-moon/keystone/tests/unit/test_kvs.py b/keystone-moon/keystone/tests/unit/test_kvs.py index 18931f5d..a88ee1ac 100644 --- a/keystone-moon/keystone/tests/unit/test_kvs.py +++ b/keystone-moon/keystone/tests/unit/test_kvs.py @@ -17,7 +17,6 @@ import uuid from dogpile.cache import api from dogpile.cache import proxy -from dogpile.cache import util import mock import six from testtools import matchers @@ -86,9 +85,12 @@ class RegionProxy2Fixture(proxy.ProxyBackend): class TestMemcacheDriver(api.CacheBackend): - """A test dogpile.cache backend that conforms to the mixin-mechanism for + """A test dogpile.cache backend. + + This test backend conforms to the mixin-mechanism for overriding set and set_multi methods on dogpile memcached drivers. """ + class test_client(object): # FIXME(morganfainberg): Convert this test client over to using mock # and/or mock.MagicMock as appropriate @@ -203,10 +205,10 @@ class KVSTest(unit.TestCase): kvs = self._get_kvs_region() kvs.configure('openstack.kvs.Memory') - self.assertIs(kvs._region.key_mangler, util.sha1_mangle_key) + self.assertIs(kvs._region.key_mangler, core.sha1_mangle_key) # The backend should also have the keymangler set the same as the # region now. - self.assertIs(kvs._region.backend.key_mangler, util.sha1_mangle_key) + self.assertIs(kvs._region.backend.key_mangler, core.sha1_mangle_key) def test_kvs_key_mangler_configuration_backend(self): kvs = self._get_kvs_region() @@ -217,7 +219,7 @@ class KVSTest(unit.TestCase): def test_kvs_key_mangler_configuration_forced_backend(self): kvs = self._get_kvs_region() kvs.configure('openstack.kvs.KVSBackendForcedKeyMangleFixture', - key_mangler=util.sha1_mangle_key) + key_mangler=core.sha1_mangle_key) expected = KVSBackendForcedKeyMangleFixture.key_mangler(self.key_foo) self.assertEqual(expected, kvs._region.key_mangler(self.key_foo)) @@ -236,7 +238,7 @@ class KVSTest(unit.TestCase): kvs = self._get_kvs_region() kvs.configure('openstack.kvs.Memory') - self.assertIs(kvs._region.backend.key_mangler, util.sha1_mangle_key) + self.assertIs(kvs._region.backend.key_mangler, core.sha1_mangle_key) kvs._set_key_mangler(test_key_mangler) self.assertIs(kvs._region.backend.key_mangler, test_key_mangler) @@ -432,7 +434,7 @@ class KVSTest(unit.TestCase): no_expiry_keys=no_expiry_keys) calculated_keys = set([kvs._region.key_mangler(key) for key in no_expiry_keys]) - self.assertIs(kvs._region.backend.key_mangler, util.sha1_mangle_key) + self.assertIs(kvs._region.backend.key_mangler, core.sha1_mangle_key) self.assertSetEqual(calculated_keys, kvs._region.backend.no_expiry_hashed_keys) self.assertSetEqual(no_expiry_keys, @@ -450,7 +452,7 @@ class KVSTest(unit.TestCase): kvs.configure('openstack.kvs.Memcached', memcached_backend='TestDriver', no_expiry_keys=no_expiry_keys) - self.assertIs(kvs._region.backend.key_mangler, util.sha1_mangle_key) + self.assertIs(kvs._region.backend.key_mangler, core.sha1_mangle_key) kvs._region.backend.key_mangler = None self.assertSetEqual(kvs._region.backend.raw_no_expiry_keys, kvs._region.backend.no_expiry_hashed_keys) @@ -492,15 +494,15 @@ class KVSTest(unit.TestCase): # Ensure the set_arguments are correct self.assertDictEqual( - kvs._region.backend._get_set_arguments_driver_attr(), - expected_set_args) + expected_set_args, + kvs._region.backend._get_set_arguments_driver_attr()) # Set a key that would have an expiry and verify the correct result # occurred and that the correct set_arguments were passed. kvs.set(self.key_foo, self.value_foo) self.assertDictEqual( - kvs._region.backend.driver.client.set_arguments_passed, - expected_set_args) + expected_set_args, + kvs._region.backend.driver.client.set_arguments_passed) observed_foo_keys = list(kvs_driver.client.keys_values.keys()) self.assertEqual(expected_foo_keys, observed_foo_keys) self.assertEqual( @@ -511,8 +513,8 @@ class KVSTest(unit.TestCase): # occurred and that the correct set_arguments were passed. kvs.set(self.key_bar, self.value_bar) self.assertDictEqual( - kvs._region.backend.driver.client.set_arguments_passed, - expected_no_expiry_args) + expected_no_expiry_args, + kvs._region.backend.driver.client.set_arguments_passed) observed_bar_keys = list(kvs_driver.client.keys_values.keys()) self.assertEqual(expected_bar_keys, observed_bar_keys) self.assertEqual( @@ -523,8 +525,8 @@ class KVSTest(unit.TestCase): # result occurred and that the correct set_arguments were passed. kvs.set_multi(mapping_foo) self.assertDictEqual( - kvs._region.backend.driver.client.set_arguments_passed, - expected_set_args) + expected_set_args, + kvs._region.backend.driver.client.set_arguments_passed) observed_foo_keys = list(kvs_driver.client.keys_values.keys()) self.assertEqual(expected_foo_keys, observed_foo_keys) self.assertEqual( @@ -535,8 +537,8 @@ class KVSTest(unit.TestCase): # result occurred and that the correct set_arguments were passed. kvs.set_multi(mapping_bar) self.assertDictEqual( - kvs._region.backend.driver.client.set_arguments_passed, - expected_no_expiry_args) + expected_no_expiry_args, + kvs._region.backend.driver.client.set_arguments_passed) observed_bar_keys = list(kvs_driver.client.keys_values.keys()) self.assertEqual(expected_bar_keys, observed_bar_keys) self.assertEqual( diff --git a/keystone-moon/keystone/tests/unit/test_ldap_livetest.py b/keystone-moon/keystone/tests/unit/test_ldap_livetest.py index e2abd56d..4bce6a73 100644 --- a/keystone-moon/keystone/tests/unit/test_ldap_livetest.py +++ b/keystone-moon/keystone/tests/unit/test_ldap_livetest.py @@ -69,9 +69,6 @@ class LiveLDAPIdentity(test_backend_ldap.LDAPIdentity): create_object(CONF.ldap.role_tree_dn, {'objectclass': 'organizationalUnit', 'ou': 'Roles'}) - create_object(CONF.ldap.project_tree_dn, - {'objectclass': 'organizationalUnit', - 'ou': 'Projects'}) create_object(CONF.ldap.group_tree_dn, {'objectclass': 'organizationalUnit', 'ou': 'UserGroups'}) @@ -82,8 +79,7 @@ class LiveLDAPIdentity(test_backend_ldap.LDAPIdentity): return config_files def test_build_tree(self): - """Regression test for building the tree names - """ + """Regression test for building the tree names.""" # logic is different from the fake backend. user_api = identity_ldap.UserApi(CONF) self.assertTrue(user_api) @@ -134,6 +130,7 @@ class LiveLDAPIdentity(test_backend_ldap.LDAPIdentity): USER_COUNT = 2 for x in range(0, USER_COUNT): + # TODO(shaleh): use unit.new_user_ref() new_user = {'name': uuid.uuid4().hex, 'password': uuid.uuid4().hex, 'enabled': True, 'domain_id': domain['id']} new_user = self.identity_api.create_user(new_user) @@ -147,8 +144,7 @@ class LiveLDAPIdentity(test_backend_ldap.LDAPIdentity): self.assertEqual(0, len(group_refs)) for x in range(0, GROUP_COUNT): - new_group = {'domain_id': domain['id'], - 'name': uuid.uuid4().hex} + new_group = unit.new_group_ref(domain_id=domain['id']) new_group = self.identity_api.create_group(new_group) test_groups.append(new_group) diff --git a/keystone-moon/keystone/tests/unit/test_ldap_pool_livetest.py b/keystone-moon/keystone/tests/unit/test_ldap_pool_livetest.py index 81e91ce5..a284114a 100644 --- a/keystone-moon/keystone/tests/unit/test_ldap_pool_livetest.py +++ b/keystone-moon/keystone/tests/unit/test_ldap_pool_livetest.py @@ -105,6 +105,7 @@ class LiveLDAPPoolIdentity(test_backend_ldap_pool.LdapPoolCommonTestMixin, password=old_password) def _create_user_and_authenticate(self, password): + # TODO(shaleh): port to new_user_ref() user_dict = { 'domain_id': CONF.identity.default_domain_id, 'name': uuid.uuid4().hex, @@ -183,7 +184,7 @@ class LiveLDAPPoolIdentity(test_backend_ldap_pool.LdapPoolCommonTestMixin, user_ref = self.identity_api.authenticate( context={}, user_id=user['id'], password=old_password) - self.assertDictEqual(user_ref, user) + self.assertDictEqual(user, user_ref) def test_password_change_with_auth_pool_enabled_no_lifetime(self): self.config_fixture.config(group='ldap', diff --git a/keystone-moon/keystone/tests/unit/test_ldap_tls_livetest.py b/keystone-moon/keystone/tests/unit/test_ldap_tls_livetest.py index 6b47bfd9..98e2882d 100644 --- a/keystone-moon/keystone/tests/unit/test_ldap_tls_livetest.py +++ b/keystone-moon/keystone/tests/unit/test_ldap_tls_livetest.py @@ -50,6 +50,7 @@ class LiveTLSLDAPIdentity(test_ldap_livetest.LiveLDAPIdentity): tls_req_cert='demand') self.identity_api = identity.backends.ldap.Identity() + # TODO(shaleh): use new_user_ref() user = {'name': 'fake1', 'password': 'fakepass1', 'tenants': ['bar']} @@ -71,6 +72,7 @@ class LiveTLSLDAPIdentity(test_ldap_livetest.LiveLDAPIdentity): tls_req_cert='demand') self.identity_api = identity.backends.ldap.Identity() + # TODO(shaleh): use new_user_ref() user = {'id': 'fake1', 'name': 'fake1', 'password': 'fakepass1', @@ -95,6 +97,7 @@ class LiveTLSLDAPIdentity(test_ldap_livetest.LiveLDAPIdentity): tls_cacertdir=None) self.identity_api = identity.backends.ldap.Identity() + # TODO(shaleh): use new_user_ref() user = {'name': 'fake1', 'password': 'fakepass1', 'tenants': ['bar']} @@ -109,6 +112,7 @@ class LiveTLSLDAPIdentity(test_ldap_livetest.LiveLDAPIdentity): tls_cacertdir='/etc/keystone/ssl/mythicalcertdir') self.identity_api = identity.backends.ldap.Identity() + # TODO(shaleh): use new_user_ref() user = {'name': 'fake1', 'password': 'fakepass1', 'tenants': ['bar']} diff --git a/keystone-moon/keystone/tests/unit/test_middleware.py b/keystone-moon/keystone/tests/unit/test_middleware.py index 0eedb9c6..d33e8c00 100644 --- a/keystone-moon/keystone/tests/unit/test_middleware.py +++ b/keystone-moon/keystone/tests/unit/test_middleware.py @@ -12,17 +12,18 @@ # License for the specific language governing permissions and limitations # under the License. +import copy import hashlib import uuid from oslo_config import cfg from six.moves import http_client -import webob +import webtest from keystone.common import authorization from keystone.common import tokenless_auth -from keystone.contrib.federation import constants as federation_constants from keystone import exception +from keystone.federation import constants as federation_constants from keystone import middleware from keystone.tests import unit from keystone.tests.unit import mapping_fixtures @@ -32,104 +33,158 @@ from keystone.tests.unit import test_backend_sql CONF = cfg.CONF -def make_request(**kwargs): - accept = kwargs.pop('accept', None) - method = kwargs.pop('method', 'GET') - body = kwargs.pop('body', None) - req = webob.Request.blank('/', **kwargs) - req.method = method - if body is not None: - req.body = body - if accept is not None: - req.accept = accept - return req +class MiddlewareRequestTestBase(unit.TestCase): + MIDDLEWARE_CLASS = None # override this in subclasses -def make_response(**kwargs): - body = kwargs.pop('body', None) - return webob.Response(body) + def _application(self): + """A base wsgi application that returns a simple response.""" + def app(environ, start_response): + # WSGI requires the body of the response to be six.binary_type + body = uuid.uuid4().hex.encode('utf-8') + resp_headers = [('Content-Type', 'text/html; charset=utf8'), + ('Content-Length', str(len(body)))] + start_response('200 OK', resp_headers) + return [body] + return app + + def _generate_app_response(self, app, headers=None, method='get', + path='/', **kwargs): + """Given a wsgi application wrap it in webtest and call it.""" + return getattr(webtest.TestApp(app), method)(path, + headers=headers or {}, + **kwargs) + + def _middleware_failure(self, exc, *args, **kwargs): + """Assert that an exception is being thrown from process_request.""" + # NOTE(jamielennox): This is a little ugly. We need to call the webtest + # framework so that the correct RequestClass object is created for when + # we call process_request. However because we go via webtest we only + # see the response object and not the actual exception that is thrown + # by process_request. To get around this we subclass process_request + # with something that checks for the right type of exception being + # thrown so we can test the middle of the request process. + # TODO(jamielennox): Change these tests to test the value of the + # response rather than the error that is raised. + + class _Failing(self.MIDDLEWARE_CLASS): + + _called = False + + def process_request(i_self, *i_args, **i_kwargs): + # i_ to distinguish it from and not clobber the outer vars + e = self.assertRaises(exc, + super(_Failing, i_self).process_request, + *i_args, **i_kwargs) + i_self._called = True + raise e + + # by default the returned status when an uncaught exception is raised + # for validation or caught errors this will likely be 400 + kwargs.setdefault('status', http_client.INTERNAL_SERVER_ERROR) # 500 + + app = _Failing(self._application()) + resp = self._generate_app_response(app, *args, **kwargs) + self.assertTrue(app._called) + return resp + + def _do_middleware_response(self, *args, **kwargs): + """Wrap a middleware around a sample application and call it.""" + app = self.MIDDLEWARE_CLASS(self._application()) + return self._generate_app_response(app, *args, **kwargs) + + def _do_middleware_request(self, *args, **kwargs): + """The request object from a successful middleware call.""" + return self._do_middleware_response(*args, **kwargs).request + + +class TokenAuthMiddlewareTest(MiddlewareRequestTestBase): + + MIDDLEWARE_CLASS = middleware.TokenAuthMiddleware -class TokenAuthMiddlewareTest(unit.TestCase): def test_request(self): - req = make_request() - req.headers[middleware.AUTH_TOKEN_HEADER] = 'MAGIC' - middleware.TokenAuthMiddleware(None).process_request(req) + headers = {middleware.AUTH_TOKEN_HEADER: 'MAGIC'} + req = self._do_middleware_request(headers=headers) context = req.environ[middleware.CONTEXT_ENV] self.assertEqual('MAGIC', context['token_id']) -class AdminTokenAuthMiddlewareTest(unit.TestCase): +class AdminTokenAuthMiddlewareTest(MiddlewareRequestTestBase): + + MIDDLEWARE_CLASS = middleware.AdminTokenAuthMiddleware + + def config_overrides(self): + super(AdminTokenAuthMiddlewareTest, self).config_overrides() + self.config_fixture.config( + admin_token='ADMIN') + def test_request_admin(self): - req = make_request() - req.headers[middleware.AUTH_TOKEN_HEADER] = CONF.admin_token - middleware.AdminTokenAuthMiddleware(None).process_request(req) - context = req.environ[middleware.CONTEXT_ENV] - self.assertTrue(context['is_admin']) + headers = {middleware.AUTH_TOKEN_HEADER: 'ADMIN'} + req = self._do_middleware_request(headers=headers) + self.assertTrue(req.environ[middleware.CONTEXT_ENV]['is_admin']) def test_request_non_admin(self): - req = make_request() - req.headers[middleware.AUTH_TOKEN_HEADER] = 'NOT-ADMIN' - middleware.AdminTokenAuthMiddleware(None).process_request(req) - context = req.environ[middleware.CONTEXT_ENV] - self.assertFalse(context['is_admin']) + headers = {middleware.AUTH_TOKEN_HEADER: 'NOT-ADMIN'} + req = self._do_middleware_request(headers=headers) + self.assertFalse(req.environ[middleware.CONTEXT_ENV]['is_admin']) -class PostParamsMiddlewareTest(unit.TestCase): - def test_request_with_params(self): - req = make_request(body="arg1=one", method='POST') - middleware.PostParamsMiddleware(None).process_request(req) - params = req.environ[middleware.PARAMS_ENV] - self.assertEqual({"arg1": "one"}, params) +class JsonBodyMiddlewareTest(MiddlewareRequestTestBase): + MIDDLEWARE_CLASS = middleware.JsonBodyMiddleware -class JsonBodyMiddlewareTest(unit.TestCase): def test_request_with_params(self): - req = make_request(body='{"arg1": "one", "arg2": ["a"]}', - content_type='application/json', - method='POST') - middleware.JsonBodyMiddleware(None).process_request(req) - params = req.environ[middleware.PARAMS_ENV] - self.assertEqual({"arg1": "one", "arg2": ["a"]}, params) + headers = {'Content-Type': 'application/json'} + params = '{"arg1": "one", "arg2": ["a"]}' + req = self._do_middleware_request(params=params, + headers=headers, + method='post') + self.assertEqual({"arg1": "one", "arg2": ["a"]}, + req.environ[middleware.PARAMS_ENV]) def test_malformed_json(self): - req = make_request(body='{"arg1": "on', - content_type='application/json', - method='POST') - resp = middleware.JsonBodyMiddleware(None).process_request(req) - self.assertEqual(http_client.BAD_REQUEST, resp.status_int) + headers = {'Content-Type': 'application/json'} + self._do_middleware_response(params='{"arg1": "on', + headers=headers, + method='post', + status=http_client.BAD_REQUEST) def test_not_dict_body(self): - req = make_request(body='42', - content_type='application/json', - method='POST') - resp = middleware.JsonBodyMiddleware(None).process_request(req) - self.assertEqual(http_client.BAD_REQUEST, resp.status_int) - self.assertTrue('valid JSON object' in resp.json['error']['message']) + headers = {'Content-Type': 'application/json'} + resp = self._do_middleware_response(params='42', + headers=headers, + method='post', + status=http_client.BAD_REQUEST) + + self.assertIn('valid JSON object', resp.json['error']['message']) def test_no_content_type(self): - req = make_request(body='{"arg1": "one", "arg2": ["a"]}', - method='POST') - middleware.JsonBodyMiddleware(None).process_request(req) - params = req.environ[middleware.PARAMS_ENV] - self.assertEqual({"arg1": "one", "arg2": ["a"]}, params) + headers = {'Content-Type': ''} + params = '{"arg1": "one", "arg2": ["a"]}' + req = self._do_middleware_request(params=params, + headers=headers, + method='post') + self.assertEqual({"arg1": "one", "arg2": ["a"]}, + req.environ[middleware.PARAMS_ENV]) def test_unrecognized_content_type(self): - req = make_request(body='{"arg1": "one", "arg2": ["a"]}', - content_type='text/plain', - method='POST') - resp = middleware.JsonBodyMiddleware(None).process_request(req) - self.assertEqual(http_client.BAD_REQUEST, resp.status_int) + headers = {'Content-Type': 'text/plain'} + self._do_middleware_response(params='{"arg1": "one", "arg2": ["a"]}', + headers=headers, + method='post', + status=http_client.BAD_REQUEST) def test_unrecognized_content_type_without_body(self): - req = make_request(content_type='text/plain', - method='GET') - middleware.JsonBodyMiddleware(None).process_request(req) - params = req.environ.get(middleware.PARAMS_ENV, {}) - self.assertEqual({}, params) + headers = {'Content-Type': 'text/plain'} + req = self._do_middleware_request(headers=headers) + self.assertEqual({}, req.environ.get(middleware.PARAMS_ENV, {})) + +class AuthContextMiddlewareTest(test_backend_sql.SqlTests, + MiddlewareRequestTestBase): -class AuthContextMiddlewareTest(test_backend_sql.SqlTests): + MIDDLEWARE_CLASS = middleware.AuthContextMiddleware def setUp(self): super(AuthContextMiddlewareTest, self).setUp() @@ -139,55 +194,32 @@ class AuthContextMiddlewareTest(test_backend_sql.SqlTests): self.config_fixture.config(group='tokenless_auth', trusted_issuer=[self.trusted_issuer]) - # This idp_id is calculated based on - # sha256(self.client_issuer) - hashed_idp = hashlib.sha256(self.client_issuer) + # client_issuer is encoded because you can't hash + # unicode objects with hashlib. + # This idp_id is calculated based on sha256(self.client_issuer) + hashed_idp = hashlib.sha256(self.client_issuer.encode('utf-8')) self.idp_id = hashed_idp.hexdigest() self._load_sample_data() def _load_sample_data(self): - self.domain_id = uuid.uuid4().hex - self.domain_name = uuid.uuid4().hex - self.project_id = uuid.uuid4().hex - self.project_name = uuid.uuid4().hex - self.user_name = uuid.uuid4().hex - self.user_password = uuid.uuid4().hex - self.user_email = uuid.uuid4().hex self.protocol_id = 'x509' - self.role_id = uuid.uuid4().hex - self.role_name = uuid.uuid4().hex - # for ephemeral user - self.group_name = uuid.uuid4().hex # 1) Create a domain for the user. - self.domain = { - 'description': uuid.uuid4().hex, - 'enabled': True, - 'id': self.domain_id, - 'name': self.domain_name, - } - + self.domain = unit.new_domain_ref() + self.domain_id = self.domain['id'] + self.domain_name = self.domain['name'] self.resource_api.create_domain(self.domain_id, self.domain) # 2) Create a project for the user. - self.project = { - 'description': uuid.uuid4().hex, - 'domain_id': self.domain_id, - 'enabled': True, - 'id': self.project_id, - 'name': self.project_name, - } + self.project = unit.new_project_ref(domain_id=self.domain_id) + self.project_id = self.project['id'] + self.project_name = self.project['name'] self.resource_api.create_project(self.project_id, self.project) # 3) Create a user in new domain. - self.user = { - 'name': self.user_name, - 'domain_id': self.domain_id, - 'project_id': self.project_id, - 'password': self.user_password, - 'email': self.user_email, - } + self.user = unit.new_user_ref(domain_id=self.domain_id, + project_id=self.project_id) self.user = self.identity_api.create_user(self.user) @@ -197,17 +229,13 @@ class AuthContextMiddlewareTest(test_backend_sql.SqlTests): self.idp) # Add a role - self.role = { - 'id': self.role_id, - 'name': self.role_name, - } + self.role = unit.new_role_ref() + self.role_id = self.role['id'] + self.role_name = self.role['name'] self.role_api.create_role(self.role_id, self.role) # Add a group - self.group = { - 'name': self.group_name, - 'domain_id': self.domain_id, - } + self.group = unit.new_group_ref(domain_id=self.domain_id) self.group = self.identity_api.create_group(self.group) # Assign a role to the user on a project @@ -282,7 +310,7 @@ class AuthContextMiddlewareTest(test_backend_sql.SqlTests): :param request: HTTP request :param mapping_ref: A mapping in JSON structure will be setup in the - backend DB for mapping an user or a group. + backend DB for mapping a user or a group. :param exception_expected: Sets to True when an exception is expected to raised based on the given arguments. :returns: context an auth context contains user and role information @@ -300,30 +328,27 @@ class AuthContextMiddlewareTest(test_backend_sql.SqlTests): return context def test_context_already_exists(self): - req = make_request() - token_id = uuid.uuid4().hex - req.environ[authorization.AUTH_CONTEXT_ENV] = {'token_id': token_id} - context = self._create_context(request=req) - self.assertEqual(token_id, context['token_id']) + stub_value = uuid.uuid4().hex + env = {authorization.AUTH_CONTEXT_ENV: stub_value} + req = self._do_middleware_request(extra_environ=env) + self.assertEqual(stub_value, + req.environ.get(authorization.AUTH_CONTEXT_ENV)) def test_not_applicable_to_token_request(self): - env = {} - env['PATH_INFO'] = '/auth/tokens' - env['REQUEST_METHOD'] = 'POST' - req = make_request(environ=env) - context = self._create_context(request=req) + req = self._do_middleware_request(path='/auth/tokens', method='post') + context = req.environ.get(authorization.AUTH_CONTEXT_ENV) self.assertIsNone(context) def test_no_tokenless_attributes_request(self): - req = make_request() - context = self._create_context(request=req) + req = self._do_middleware_request() + context = req.environ.get(authorization.AUTH_CONTEXT_ENV) self.assertIsNone(context) def test_no_issuer_attribute_request(self): env = {} env['HTTP_X_PROJECT_ID'] = uuid.uuid4().hex - req = make_request(environ=env) - context = self._create_context(request=req) + req = self._do_middleware_request(extra_environ=env) + context = req.environ.get(authorization.AUTH_CONTEXT_ENV) self.assertIsNone(context) def test_has_only_issuer_and_project_name_request(self): @@ -332,61 +357,51 @@ class AuthContextMiddlewareTest(test_backend_sql.SqlTests): # references to issuer of the client certificate. env['SSL_CLIENT_I_DN'] = self.client_issuer env['HTTP_X_PROJECT_NAME'] = uuid.uuid4().hex - req = make_request(environ=env) - context = self._create_context(request=req, - exception_expected=True) - self.assertRaises(exception.ValidationError, - context.process_request, - req) + self._middleware_failure(exception.ValidationError, + extra_environ=env, + status=400) def test_has_only_issuer_and_project_domain_name_request(self): env = {} env['SSL_CLIENT_I_DN'] = self.client_issuer env['HTTP_X_PROJECT_DOMAIN_NAME'] = uuid.uuid4().hex - req = make_request(environ=env) - context = self._create_context(request=req, - exception_expected=True) - self.assertRaises(exception.ValidationError, - context.process_request, - req) + self._middleware_failure(exception.ValidationError, + extra_environ=env, + status=400) def test_has_only_issuer_and_project_domain_id_request(self): env = {} env['SSL_CLIENT_I_DN'] = self.client_issuer env['HTTP_X_PROJECT_DOMAIN_ID'] = uuid.uuid4().hex - req = make_request(environ=env) - context = self._create_context(request=req, - exception_expected=True) - self.assertRaises(exception.ValidationError, - context.process_request, - req) + self._middleware_failure(exception.ValidationError, + extra_environ=env, + status=400) def test_missing_both_domain_and_project_request(self): env = {} env['SSL_CLIENT_I_DN'] = self.client_issuer - req = make_request(environ=env) - context = self._create_context(request=req, - exception_expected=True) - self.assertRaises(exception.ValidationError, - context.process_request, - req) + self._middleware_failure(exception.ValidationError, + extra_environ=env, + status=400) def test_empty_trusted_issuer_list(self): env = {} env['SSL_CLIENT_I_DN'] = self.client_issuer env['HTTP_X_PROJECT_ID'] = uuid.uuid4().hex - req = make_request(environ=env) + self.config_fixture.config(group='tokenless_auth', trusted_issuer=[]) - context = self._create_context(request=req) + + req = self._do_middleware_request(extra_environ=env) + context = req.environ.get(authorization.AUTH_CONTEXT_ENV) self.assertIsNone(context) def test_client_issuer_not_trusted(self): env = {} env['SSL_CLIENT_I_DN'] = self.untrusted_client_issuer env['HTTP_X_PROJECT_ID'] = uuid.uuid4().hex - req = make_request(environ=env) - context = self._create_context(request=req) + req = self._do_middleware_request(extra_environ=env) + context = req.environ.get(authorization.AUTH_CONTEXT_ENV) self.assertIsNone(context) def test_proj_scope_with_proj_id_and_proj_dom_id_success(self): @@ -397,24 +412,28 @@ class AuthContextMiddlewareTest(test_backend_sql.SqlTests): # SSL_CLIENT_USER_NAME and SSL_CLIENT_DOMAIN_NAME are the types # defined in the mapping that will map to the user name and # domain name - env['SSL_CLIENT_USER_NAME'] = self.user_name + env['SSL_CLIENT_USER_NAME'] = self.user['name'] env['SSL_CLIENT_DOMAIN_NAME'] = self.domain_name - req = make_request(environ=env) - context = self._create_context( - request=req, - mapping_ref=mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINNAME) + + self._load_mapping_rules( + mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINNAME) + + req = self._do_middleware_request(extra_environ=env) + context = req.environ.get(authorization.AUTH_CONTEXT_ENV) self._assert_tokenless_auth_context(context) def test_proj_scope_with_proj_id_only_success(self): env = {} env['SSL_CLIENT_I_DN'] = self.client_issuer env['HTTP_X_PROJECT_ID'] = self.project_id - env['SSL_CLIENT_USER_NAME'] = self.user_name + env['SSL_CLIENT_USER_NAME'] = self.user['name'] env['SSL_CLIENT_DOMAIN_NAME'] = self.domain_name - req = make_request(environ=env) - context = self._create_context( - request=req, - mapping_ref=mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINNAME) + + self._load_mapping_rules( + mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINNAME) + + req = self._do_middleware_request(extra_environ=env) + context = req.environ.get(authorization.AUTH_CONTEXT_ENV) self._assert_tokenless_auth_context(context) def test_proj_scope_with_proj_name_and_proj_dom_id_success(self): @@ -422,12 +441,14 @@ class AuthContextMiddlewareTest(test_backend_sql.SqlTests): env['SSL_CLIENT_I_DN'] = self.client_issuer env['HTTP_X_PROJECT_NAME'] = self.project_name env['HTTP_X_PROJECT_DOMAIN_ID'] = self.domain_id - env['SSL_CLIENT_USER_NAME'] = self.user_name + env['SSL_CLIENT_USER_NAME'] = self.user['name'] env['SSL_CLIENT_DOMAIN_NAME'] = self.domain_name - req = make_request(environ=env) - context = self._create_context( - request=req, - mapping_ref=mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINNAME) + + self._load_mapping_rules( + mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINNAME) + + req = self._do_middleware_request(extra_environ=env) + context = req.environ.get(authorization.AUTH_CONTEXT_ENV) self._assert_tokenless_auth_context(context) def test_proj_scope_with_proj_name_and_proj_dom_name_success(self): @@ -435,28 +456,29 @@ class AuthContextMiddlewareTest(test_backend_sql.SqlTests): env['SSL_CLIENT_I_DN'] = self.client_issuer env['HTTP_X_PROJECT_NAME'] = self.project_name env['HTTP_X_PROJECT_DOMAIN_NAME'] = self.domain_name - env['SSL_CLIENT_USER_NAME'] = self.user_name + env['SSL_CLIENT_USER_NAME'] = self.user['name'] env['SSL_CLIENT_DOMAIN_NAME'] = self.domain_name - req = make_request(environ=env) - context = self._create_context( - request=req, - mapping_ref=mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINNAME) + + self._load_mapping_rules( + mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINNAME) + + req = self._do_middleware_request(extra_environ=env) + context = req.environ.get(authorization.AUTH_CONTEXT_ENV) self._assert_tokenless_auth_context(context) def test_proj_scope_with_proj_name_only_fail(self): env = {} env['SSL_CLIENT_I_DN'] = self.client_issuer env['HTTP_X_PROJECT_NAME'] = self.project_id - env['SSL_CLIENT_USER_NAME'] = self.user_name + env['SSL_CLIENT_USER_NAME'] = self.user['name'] env['SSL_CLIENT_DOMAIN_NAME'] = self.domain_name - req = make_request(environ=env) - context = self._create_context( - request=req, - mapping_ref=mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINNAME, - exception_expected=True) - self.assertRaises(exception.ValidationError, - context.process_request, - req) + + self._load_mapping_rules( + mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINNAME) + + self._middleware_failure(exception.ValidationError, + extra_environ=env, + status=400) def test_mapping_with_userid_and_domainid_success(self): env = {} @@ -465,10 +487,12 @@ class AuthContextMiddlewareTest(test_backend_sql.SqlTests): env['HTTP_X_PROJECT_DOMAIN_NAME'] = self.domain_name env['SSL_CLIENT_USER_ID'] = self.user['id'] env['SSL_CLIENT_DOMAIN_ID'] = self.domain_id - req = make_request(environ=env) - context = self._create_context( - request=req, - mapping_ref=mapping_fixtures.MAPPING_WITH_USERID_AND_DOMAINID) + + self._load_mapping_rules( + mapping_fixtures.MAPPING_WITH_USERID_AND_DOMAINID) + + req = self._do_middleware_request(extra_environ=env) + context = req.environ.get(authorization.AUTH_CONTEXT_ENV) self._assert_tokenless_auth_context(context) def test_mapping_with_userid_and_domainname_success(self): @@ -478,10 +502,12 @@ class AuthContextMiddlewareTest(test_backend_sql.SqlTests): env['HTTP_X_PROJECT_DOMAIN_NAME'] = self.domain_name env['SSL_CLIENT_USER_ID'] = self.user['id'] env['SSL_CLIENT_DOMAIN_NAME'] = self.domain_name - req = make_request(environ=env) - context = self._create_context( - request=req, - mapping_ref=mapping_fixtures.MAPPING_WITH_USERID_AND_DOMAINNAME) + + self._load_mapping_rules( + mapping_fixtures.MAPPING_WITH_USERID_AND_DOMAINNAME) + + req = self._do_middleware_request(extra_environ=env) + context = req.environ.get(authorization.AUTH_CONTEXT_ENV) self._assert_tokenless_auth_context(context) def test_mapping_with_username_and_domainid_success(self): @@ -489,12 +515,14 @@ class AuthContextMiddlewareTest(test_backend_sql.SqlTests): env['SSL_CLIENT_I_DN'] = self.client_issuer env['HTTP_X_PROJECT_NAME'] = self.project_name env['HTTP_X_PROJECT_DOMAIN_NAME'] = self.domain_name - env['SSL_CLIENT_USER_NAME'] = self.user_name + env['SSL_CLIENT_USER_NAME'] = self.user['name'] env['SSL_CLIENT_DOMAIN_ID'] = self.domain_id - req = make_request(environ=env) - context = self._create_context( - request=req, - mapping_ref=mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINID) + + self._load_mapping_rules( + mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINID) + + req = self._do_middleware_request(extra_environ=env) + context = req.environ.get(authorization.AUTH_CONTEXT_ENV) self._assert_tokenless_auth_context(context) def test_only_domain_name_fail(self): @@ -503,14 +531,13 @@ class AuthContextMiddlewareTest(test_backend_sql.SqlTests): env['HTTP_X_PROJECT_ID'] = self.project_id env['HTTP_X_PROJECT_DOMAIN_ID'] = self.domain_id env['SSL_CLIENT_DOMAIN_NAME'] = self.domain_name - req = make_request(environ=env) - context = self._create_context( - request=req, - mapping_ref=mapping_fixtures.MAPPING_WITH_DOMAINNAME_ONLY, - exception_expected=True) - self.assertRaises(exception.ValidationError, - context.process_request, - req) + + self._load_mapping_rules( + mapping_fixtures.MAPPING_WITH_DOMAINNAME_ONLY) + + self._middleware_failure(exception.ValidationError, + extra_environ=env, + status=400) def test_only_domain_id_fail(self): env = {} @@ -518,29 +545,27 @@ class AuthContextMiddlewareTest(test_backend_sql.SqlTests): env['HTTP_X_PROJECT_ID'] = self.project_id env['HTTP_X_PROJECT_DOMAIN_ID'] = self.domain_id env['SSL_CLIENT_DOMAIN_ID'] = self.domain_id - req = make_request(environ=env) - context = self._create_context( - request=req, - mapping_ref=mapping_fixtures.MAPPING_WITH_DOMAINID_ONLY, - exception_expected=True) - self.assertRaises(exception.ValidationError, - context.process_request, - req) + + self._load_mapping_rules( + mapping_fixtures.MAPPING_WITH_DOMAINID_ONLY) + + self._middleware_failure(exception.ValidationError, + extra_environ=env, + status=400) def test_missing_domain_data_fail(self): env = {} env['SSL_CLIENT_I_DN'] = self.client_issuer env['HTTP_X_PROJECT_ID'] = self.project_id env['HTTP_X_PROJECT_DOMAIN_ID'] = self.domain_id - env['SSL_CLIENT_USER_NAME'] = self.user_name - req = make_request(environ=env) - context = self._create_context( - request=req, - mapping_ref=mapping_fixtures.MAPPING_WITH_USERNAME_ONLY, - exception_expected=True) - self.assertRaises(exception.ValidationError, - context.process_request, - req) + env['SSL_CLIENT_USER_NAME'] = self.user['name'] + + self._load_mapping_rules( + mapping_fixtures.MAPPING_WITH_USERNAME_ONLY) + + self._middleware_failure(exception.ValidationError, + extra_environ=env, + status=400) def test_userid_success(self): env = {} @@ -548,10 +573,10 @@ class AuthContextMiddlewareTest(test_backend_sql.SqlTests): env['HTTP_X_PROJECT_ID'] = self.project_id env['HTTP_X_PROJECT_DOMAIN_ID'] = self.domain_id env['SSL_CLIENT_USER_ID'] = self.user['id'] - req = make_request(environ=env) - context = self._create_context( - request=req, - mapping_ref=mapping_fixtures.MAPPING_WITH_USERID_ONLY) + + self._load_mapping_rules(mapping_fixtures.MAPPING_WITH_USERID_ONLY) + req = self._do_middleware_request(extra_environ=env) + context = req.environ.get(authorization.AUTH_CONTEXT_ENV) self._assert_tokenless_auth_context(context) def test_domain_disable_fail(self): @@ -559,37 +584,35 @@ class AuthContextMiddlewareTest(test_backend_sql.SqlTests): env['SSL_CLIENT_I_DN'] = self.client_issuer env['HTTP_X_PROJECT_NAME'] = self.project_name env['HTTP_X_PROJECT_DOMAIN_NAME'] = self.domain_name - env['SSL_CLIENT_USER_NAME'] = self.user_name + env['SSL_CLIENT_USER_NAME'] = self.user['name'] env['SSL_CLIENT_DOMAIN_ID'] = self.domain_id - req = make_request(environ=env) + self.domain['enabled'] = False self.domain = self.resource_api.update_domain( self.domain['id'], self.domain) - context = self._create_context( - request=req, - mapping_ref=mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINID, - exception_expected=True) - self.assertRaises(exception.Unauthorized, - context.process_request, - req) + + self._load_mapping_rules( + mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINID) + self._middleware_failure(exception.Unauthorized, + extra_environ=env, + status=401) def test_user_disable_fail(self): env = {} env['SSL_CLIENT_I_DN'] = self.client_issuer env['HTTP_X_PROJECT_NAME'] = self.project_name env['HTTP_X_PROJECT_DOMAIN_NAME'] = self.domain_name - env['SSL_CLIENT_USER_NAME'] = self.user_name + env['SSL_CLIENT_USER_NAME'] = self.user['name'] env['SSL_CLIENT_DOMAIN_ID'] = self.domain_id - req = make_request(environ=env) + self.user['enabled'] = False self.user = self.identity_api.update_user(self.user['id'], self.user) - context = self._create_context( - request=req, - mapping_ref=mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINID, - exception_expected=True) - self.assertRaises(AssertionError, - context.process_request, - req) + + self._load_mapping_rules( + mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINID) + + self._middleware_failure(AssertionError, + extra_environ=env) def test_invalid_user_fail(self): env = {} @@ -598,30 +621,29 @@ class AuthContextMiddlewareTest(test_backend_sql.SqlTests): env['HTTP_X_PROJECT_DOMAIN_ID'] = self.domain_id env['SSL_CLIENT_USER_NAME'] = uuid.uuid4().hex env['SSL_CLIENT_DOMAIN_NAME'] = self.domain_name - req = make_request(environ=env) - context = self._create_context( - request=req, - mapping_ref=mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINNAME, - exception_expected=True) - self.assertRaises(exception.UserNotFound, - context.process_request, - req) + + self._load_mapping_rules( + mapping_fixtures.MAPPING_WITH_USERNAME_AND_DOMAINNAME) + + self._middleware_failure(exception.UserNotFound, + extra_environ=env, + status=404) def test_ephemeral_success(self): env = {} env['SSL_CLIENT_I_DN'] = self.client_issuer env['HTTP_X_PROJECT_NAME'] = self.project_name env['HTTP_X_PROJECT_DOMAIN_NAME'] = self.domain_name - env['SSL_CLIENT_USER_NAME'] = self.user_name - req = make_request(environ=env) + env['SSL_CLIENT_USER_NAME'] = self.user['name'] self.config_fixture.config(group='tokenless_auth', protocol='ephemeral') self.protocol_id = 'ephemeral' - mapping = mapping_fixtures.MAPPING_FOR_EPHEMERAL_USER.copy() + mapping = copy.deepcopy(mapping_fixtures.MAPPING_FOR_EPHEMERAL_USER) mapping['rules'][0]['local'][0]['group']['id'] = self.group['id'] - context = self._create_context( - request=req, - mapping_ref=mapping) + self._load_mapping_rules(mapping) + + req = self._do_middleware_request(extra_environ=env) + context = req.environ.get(authorization.AUTH_CONTEXT_ENV) self._assert_tokenless_auth_context(context, ephemeral_user=True) def test_ephemeral_with_default_user_type_success(self): @@ -629,23 +651,25 @@ class AuthContextMiddlewareTest(test_backend_sql.SqlTests): env['SSL_CLIENT_I_DN'] = self.client_issuer env['HTTP_X_PROJECT_NAME'] = self.project_name env['HTTP_X_PROJECT_DOMAIN_NAME'] = self.domain_name - env['SSL_CLIENT_USER_NAME'] = self.user_name - req = make_request(environ=env) + env['SSL_CLIENT_USER_NAME'] = self.user['name'] self.config_fixture.config(group='tokenless_auth', protocol='ephemeral') self.protocol_id = 'ephemeral' # this mapping does not have the user type defined # and it should defaults to 'ephemeral' which is # the expected type for the test case. - mapping = mapping_fixtures.MAPPING_FOR_DEFAULT_EPHEMERAL_USER.copy() + mapping = copy.deepcopy( + mapping_fixtures.MAPPING_FOR_DEFAULT_EPHEMERAL_USER) mapping['rules'][0]['local'][0]['group']['id'] = self.group['id'] - context = self._create_context( - request=req, - mapping_ref=mapping) + self._load_mapping_rules(mapping) + + req = self._do_middleware_request(extra_environ=env) + context = req.environ.get(authorization.AUTH_CONTEXT_ENV) self._assert_tokenless_auth_context(context, ephemeral_user=True) def test_ephemeral_any_user_success(self): - """Ephemeral user does not need a specified user + """Verify ephemeral user does not need a specified user. + Keystone is not looking to match the user, but a corresponding group. """ env = {} @@ -653,15 +677,15 @@ class AuthContextMiddlewareTest(test_backend_sql.SqlTests): env['HTTP_X_PROJECT_NAME'] = self.project_name env['HTTP_X_PROJECT_DOMAIN_NAME'] = self.domain_name env['SSL_CLIENT_USER_NAME'] = uuid.uuid4().hex - req = make_request(environ=env) self.config_fixture.config(group='tokenless_auth', protocol='ephemeral') self.protocol_id = 'ephemeral' - mapping = mapping_fixtures.MAPPING_FOR_EPHEMERAL_USER.copy() + mapping = copy.deepcopy(mapping_fixtures.MAPPING_FOR_EPHEMERAL_USER) mapping['rules'][0]['local'][0]['group']['id'] = self.group['id'] - context = self._create_context( - request=req, - mapping_ref=mapping) + self._load_mapping_rules(mapping) + + req = self._do_middleware_request(extra_environ=env) + context = req.environ.get(authorization.AUTH_CONTEXT_ENV) self._assert_tokenless_auth_context(context, ephemeral_user=True) def test_ephemeral_invalid_scope_fail(self): @@ -669,43 +693,37 @@ class AuthContextMiddlewareTest(test_backend_sql.SqlTests): env['SSL_CLIENT_I_DN'] = self.client_issuer env['HTTP_X_PROJECT_NAME'] = uuid.uuid4().hex env['HTTP_X_PROJECT_DOMAIN_NAME'] = uuid.uuid4().hex - env['SSL_CLIENT_USER_NAME'] = self.user_name - req = make_request(environ=env) + env['SSL_CLIENT_USER_NAME'] = self.user['name'] self.config_fixture.config(group='tokenless_auth', protocol='ephemeral') self.protocol_id = 'ephemeral' - mapping = mapping_fixtures.MAPPING_FOR_EPHEMERAL_USER.copy() + mapping = copy.deepcopy(mapping_fixtures.MAPPING_FOR_EPHEMERAL_USER) mapping['rules'][0]['local'][0]['group']['id'] = self.group['id'] - context = self._create_context( - request=req, - mapping_ref=mapping, - exception_expected=True) - self.assertRaises(exception.Unauthorized, - context.process_request, - req) + self._load_mapping_rules(mapping) + + self._middleware_failure(exception.Unauthorized, + extra_environ=env, + status=401) def test_ephemeral_no_group_found_fail(self): env = {} env['SSL_CLIENT_I_DN'] = self.client_issuer env['HTTP_X_PROJECT_NAME'] = self.project_name env['HTTP_X_PROJECT_DOMAIN_NAME'] = self.domain_name - env['SSL_CLIENT_USER_NAME'] = self.user_name - req = make_request(environ=env) + env['SSL_CLIENT_USER_NAME'] = self.user['name'] self.config_fixture.config(group='tokenless_auth', protocol='ephemeral') self.protocol_id = 'ephemeral' - mapping = mapping_fixtures.MAPPING_FOR_EPHEMERAL_USER.copy() + mapping = copy.deepcopy(mapping_fixtures.MAPPING_FOR_EPHEMERAL_USER) mapping['rules'][0]['local'][0]['group']['id'] = uuid.uuid4().hex - context = self._create_context( - request=req, - mapping_ref=mapping, - exception_expected=True) - self.assertRaises(exception.MappedGroupNotFound, - context.process_request, - req) + self._load_mapping_rules(mapping) + + self._middleware_failure(exception.MappedGroupNotFound, + extra_environ=env) def test_ephemeral_incorrect_mapping_fail(self): - """Ephemeral user picks up the non-ephemeral user mapping. + """Test ephemeral user picking up the non-ephemeral user mapping. + Looking up the mapping with protocol Id 'x509' will load up the non-ephemeral user mapping, results unauthenticated. """ @@ -713,21 +731,17 @@ class AuthContextMiddlewareTest(test_backend_sql.SqlTests): env['SSL_CLIENT_I_DN'] = self.client_issuer env['HTTP_X_PROJECT_NAME'] = self.project_name env['HTTP_X_PROJECT_DOMAIN_NAME'] = self.domain_name - env['SSL_CLIENT_USER_NAME'] = self.user_name - req = make_request(environ=env) + env['SSL_CLIENT_USER_NAME'] = self.user['name'] # This will pick up the incorrect mapping self.config_fixture.config(group='tokenless_auth', protocol='x509') self.protocol_id = 'x509' - mapping = mapping_fixtures.MAPPING_FOR_EPHEMERAL_USER.copy() + mapping = copy.deepcopy(mapping_fixtures.MAPPING_FOR_EPHEMERAL_USER) mapping['rules'][0]['local'][0]['group']['id'] = uuid.uuid4().hex - context = self._create_context( - request=req, - mapping_ref=mapping, - exception_expected=True) - self.assertRaises(exception.MappedGroupNotFound, - context.process_request, - req) + self._load_mapping_rules(mapping) + + self._middleware_failure(exception.MappedGroupNotFound, + extra_environ=env) def test_create_idp_id_success(self): env = {} diff --git a/keystone-moon/keystone/tests/unit/test_policy.py b/keystone-moon/keystone/tests/unit/test_policy.py index 686e2b70..d6e911e9 100644 --- a/keystone-moon/keystone/tests/unit/test_policy.py +++ b/keystone-moon/keystone/tests/unit/test_policy.py @@ -23,22 +23,11 @@ from testtools import matchers from keystone import exception from keystone.policy.backends import rules from keystone.tests import unit +from keystone.tests.unit import ksfixtures from keystone.tests.unit.ksfixtures import temporaryfile -class BasePolicyTestCase(unit.TestCase): - def setUp(self): - super(BasePolicyTestCase, self).setUp() - rules.reset() - self.addCleanup(rules.reset) - self.addCleanup(self.clear_cache_safely) - - def clear_cache_safely(self): - if rules._ENFORCER: - rules._ENFORCER.clear() - - -class PolicyFileTestCase(BasePolicyTestCase): +class PolicyFileTestCase(unit.TestCase): def setUp(self): # self.tmpfilename should exist before setUp super is called # this is to ensure it is available for the config_fixture in @@ -48,10 +37,8 @@ class PolicyFileTestCase(BasePolicyTestCase): super(PolicyFileTestCase, self).setUp() self.target = {} - def config_overrides(self): - super(PolicyFileTestCase, self).config_overrides() - self.config_fixture.config(group='oslo_policy', - policy_file=self.tmpfilename) + def _policy_fixture(self): + return ksfixtures.Policy(self.tmpfilename, self.config_fixture) def test_modified_policy_reloads(self): action = "example:test" @@ -65,21 +52,10 @@ class PolicyFileTestCase(BasePolicyTestCase): self.assertRaises(exception.ForbiddenAction, rules.enforce, empty_credentials, action, self.target) - def test_invalid_policy_raises_error(self): - action = "example:test" - empty_credentials = {} - invalid_json = '{"example:test": [],}' - with open(self.tmpfilename, "w") as policyfile: - policyfile.write(invalid_json) - self.assertRaises(ValueError, rules.enforce, - empty_credentials, action, self.target) - -class PolicyTestCase(BasePolicyTestCase): +class PolicyTestCase(unit.TestCase): def setUp(self): super(PolicyTestCase, self).setUp() - # NOTE(vish): preload rules to circumvent reloading from file - rules.init() self.rules = { "true": [], "example:allowed": [], @@ -137,17 +113,16 @@ class PolicyTestCase(BasePolicyTestCase): def test_ignore_case_role_check(self): lowercase_action = "example:lowercase_admin" uppercase_action = "example:uppercase_admin" - # NOTE(dprince) we mix case in the Admin role here to ensure + # NOTE(dprince): We mix case in the Admin role here to ensure # case is ignored admin_credentials = {'roles': ['AdMiN']} rules.enforce(admin_credentials, lowercase_action, self.target) rules.enforce(admin_credentials, uppercase_action, self.target) -class DefaultPolicyTestCase(BasePolicyTestCase): +class DefaultPolicyTestCase(unit.TestCase): def setUp(self): super(DefaultPolicyTestCase, self).setUp() - rules.init() self.rules = { "default": [], @@ -160,7 +135,7 @@ class DefaultPolicyTestCase(BasePolicyTestCase): # its enforce() method even though rules has been initialized via # set_rules(). To make it easier to do our tests, we're going to # monkeypatch load_roles() so it does nothing. This seem like a bug in - # Oslo policy as we shoudn't have to reload the rules if they have + # Oslo policy as we shouldn't have to reload the rules if they have # already been set using set_rules(). self._old_load_rules = rules._ENFORCER.load_rules self.addCleanup(setattr, rules._ENFORCER, 'load_rules', diff --git a/keystone-moon/keystone/tests/unit/test_revoke.py b/keystone-moon/keystone/tests/unit/test_revoke.py index 9062981f..82c0125a 100644 --- a/keystone-moon/keystone/tests/unit/test_revoke.py +++ b/keystone-moon/keystone/tests/unit/test_revoke.py @@ -20,8 +20,8 @@ from six.moves import range from testtools import matchers from keystone.common import utils -from keystone.contrib.revoke import model from keystone import exception +from keystone.models import revoke_model from keystone.tests import unit from keystone.tests.unit import test_backend_sql from keystone.token import provider @@ -46,7 +46,7 @@ def _past_time(): def _sample_blank_token(): issued_delta = datetime.timedelta(minutes=-2) issued_at = timeutils.utcnow() + issued_delta - token_data = model.blank_token_data(issued_at) + token_data = revoke_model.blank_token_data(issued_at) return token_data @@ -61,13 +61,12 @@ def _matches(event, token_values): value for the attribute, and it does not match the token, no match is possible, so skip the remaining checks. - :param event one revocation event to match - :param token_values dictionary with set of values taken from the + :param event: one revocation event to match + :param token_values: dictionary with set of values taken from the token - :returns if the token matches the revocation event, indicating the + :returns: True if the token matches the revocation event, indicating the token has been revoked """ - # The token has three attributes that can match the user_id if event.user_id is not None: for attribute_name in ['user_id', 'trustor_id', 'trustee_id']: @@ -126,15 +125,16 @@ class RevokeTests(object): self.revoke_api.revoke_by_user(user_id=1) self.revoke_api.revoke_by_user(user_id=2) past = timeutils.utcnow() - datetime.timedelta(seconds=1000) - self.assertEqual(2, len(self.revoke_api.list_events(past))) + self.assertEqual(2, len(self.revoke_api.list_events(last_fetch=past))) future = timeutils.utcnow() + datetime.timedelta(seconds=1000) - self.assertEqual(0, len(self.revoke_api.list_events(future))) + self.assertEqual(0, + len(self.revoke_api.list_events(last_fetch=future))) def test_past_expiry_are_removed(self): user_id = 1 self.revoke_api.revoke_by_expiration(user_id, _future_time()) self.assertEqual(1, len(self.revoke_api.list_events())) - event = model.RevokeEvent() + event = revoke_model.RevokeEvent() event.revoked_at = _past_time() self.revoke_api.revoke(event) self.assertEqual(1, len(self.revoke_api.list_events())) @@ -184,32 +184,17 @@ class RevokeTests(object): class SqlRevokeTests(test_backend_sql.SqlTests, RevokeTests): def config_overrides(self): super(SqlRevokeTests, self).config_overrides() - self.config_fixture.config(group='revoke', driver='sql') self.config_fixture.config( group='token', provider='pki', revoke_by_id=False) -class KvsRevokeTests(unit.TestCase, RevokeTests): - def config_overrides(self): - super(KvsRevokeTests, self).config_overrides() - self.config_fixture.config(group='revoke', driver='kvs') - self.config_fixture.config( - group='token', - provider='pki', - revoke_by_id=False) - - def setUp(self): - super(KvsRevokeTests, self).setUp() - self.load_backends() - - class RevokeTreeTests(unit.TestCase): def setUp(self): super(RevokeTreeTests, self).setUp() self.events = [] - self.tree = model.RevokeTree() + self.tree = revoke_model.RevokeTree() self._sample_data() def _sample_data(self): @@ -263,20 +248,20 @@ class RevokeTreeTests(unit.TestCase): def _revoke_by_user(self, user_id): return self.tree.add_event( - model.RevokeEvent(user_id=user_id)) + revoke_model.RevokeEvent(user_id=user_id)) def _revoke_by_audit_id(self, audit_id): event = self.tree.add_event( - model.RevokeEvent(audit_id=audit_id)) + revoke_model.RevokeEvent(audit_id=audit_id)) self.events.append(event) return event def _revoke_by_audit_chain_id(self, audit_chain_id, project_id=None, domain_id=None): event = self.tree.add_event( - model.RevokeEvent(audit_chain_id=audit_chain_id, - project_id=project_id, - domain_id=domain_id) + revoke_model.RevokeEvent(audit_chain_id=audit_chain_id, + project_id=project_id, + domain_id=domain_id) ) self.events.append(event) return event @@ -284,46 +269,47 @@ class RevokeTreeTests(unit.TestCase): def _revoke_by_expiration(self, user_id, expires_at, project_id=None, domain_id=None): event = self.tree.add_event( - model.RevokeEvent(user_id=user_id, - expires_at=expires_at, - project_id=project_id, - domain_id=domain_id)) + revoke_model.RevokeEvent(user_id=user_id, + expires_at=expires_at, + project_id=project_id, + domain_id=domain_id)) self.events.append(event) return event def _revoke_by_grant(self, role_id, user_id=None, domain_id=None, project_id=None): event = self.tree.add_event( - model.RevokeEvent(user_id=user_id, - role_id=role_id, - domain_id=domain_id, - project_id=project_id)) + revoke_model.RevokeEvent(user_id=user_id, + role_id=role_id, + domain_id=domain_id, + project_id=project_id)) self.events.append(event) return event def _revoke_by_user_and_project(self, user_id, project_id): event = self.tree.add_event( - model.RevokeEvent(project_id=project_id, - user_id=user_id)) + revoke_model.RevokeEvent(project_id=project_id, + user_id=user_id)) self.events.append(event) return event def _revoke_by_project_role_assignment(self, project_id, role_id): event = self.tree.add_event( - model.RevokeEvent(project_id=project_id, - role_id=role_id)) + revoke_model.RevokeEvent(project_id=project_id, + role_id=role_id)) self.events.append(event) return event def _revoke_by_domain_role_assignment(self, domain_id, role_id): event = self.tree.add_event( - model.RevokeEvent(domain_id=domain_id, - role_id=role_id)) + revoke_model.RevokeEvent(domain_id=domain_id, + role_id=role_id)) self.events.append(event) return event def _revoke_by_domain(self, domain_id): - event = self.tree.add_event(model.RevokeEvent(domain_id=domain_id)) + event = self.tree.add_event( + revoke_model.RevokeEvent(domain_id=domain_id)) self.events.append(event) def _user_field_test(self, field_name): diff --git a/keystone-moon/keystone/tests/unit/test_sql_livetest.py b/keystone-moon/keystone/tests/unit/test_sql_livetest.py index e2186907..18b8ea91 100644 --- a/keystone-moon/keystone/tests/unit/test_sql_livetest.py +++ b/keystone-moon/keystone/tests/unit/test_sql_livetest.py @@ -13,7 +13,6 @@ # under the License. from keystone.tests import unit -from keystone.tests.unit import test_sql_migrate_extensions from keystone.tests.unit import test_sql_upgrade @@ -39,29 +38,6 @@ class MysqlMigrateTests(test_sql_upgrade.SqlUpgradeTests): return files -class PostgresqlRevokeExtensionsTests( - test_sql_migrate_extensions.RevokeExtension): - def setUp(self): - self.skip_if_env_not_set('ENABLE_LIVE_POSTGRES_TEST') - super(PostgresqlRevokeExtensionsTests, self).setUp() - - def config_files(self): - files = super(PostgresqlRevokeExtensionsTests, self).config_files() - files.append(unit.dirs.tests_conf("backend_postgresql.conf")) - return files - - -class MysqlRevokeExtensionsTests(test_sql_migrate_extensions.RevokeExtension): - def setUp(self): - self.skip_if_env_not_set('ENABLE_LIVE_MYSQL_TEST') - super(MysqlRevokeExtensionsTests, self).setUp() - - def config_files(self): - files = super(MysqlRevokeExtensionsTests, self).config_files() - files.append(unit.dirs.tests_conf("backend_mysql.conf")) - return files - - class Db2MigrateTests(test_sql_upgrade.SqlUpgradeTests): def setUp(self): self.skip_if_env_not_set('ENABLE_LIVE_DB2_TEST') diff --git a/keystone-moon/keystone/tests/unit/test_sql_migrate_extensions.py b/keystone-moon/keystone/tests/unit/test_sql_migrate_extensions.py index f498fe94..0155f787 100644 --- a/keystone-moon/keystone/tests/unit/test_sql_migrate_extensions.py +++ b/keystone-moon/keystone/tests/unit/test_sql_migrate_extensions.py @@ -29,369 +29,84 @@ WARNING:: all data will be lost. """ -import sqlalchemy -import uuid - -from oslo_db import exception as db_exception -from oslo_db.sqlalchemy import utils - from keystone.contrib import endpoint_filter from keystone.contrib import endpoint_policy -from keystone.contrib import example from keystone.contrib import federation from keystone.contrib import oauth1 from keystone.contrib import revoke +from keystone import exception from keystone.tests.unit import test_sql_upgrade -class SqlUpgradeExampleExtension(test_sql_upgrade.SqlMigrateBase): - def repo_package(self): - return example - - def test_upgrade(self): - self.assertTableDoesNotExist('example') - self.upgrade(1, repository=self.repo_path) - self.assertTableColumns('example', ['id', 'type', 'extra']) +class SqlUpgradeOAuth1Extension(test_sql_upgrade.SqlMigrateBase): + OAUTH1_MIGRATIONS = 5 -class SqlUpgradeOAuth1Extension(test_sql_upgrade.SqlMigrateBase): def repo_package(self): return oauth1 - def upgrade(self, version): - super(SqlUpgradeOAuth1Extension, self).upgrade( - version, repository=self.repo_path) - - def _assert_v1_3_tables(self): - self.assertTableColumns('consumer', - ['id', - 'description', - 'secret', - 'extra']) - self.assertTableColumns('request_token', - ['id', - 'request_secret', - 'verifier', - 'authorizing_user_id', - 'requested_project_id', - 'requested_roles', - 'consumer_id', - 'expires_at']) - self.assertTableColumns('access_token', - ['id', - 'access_secret', - 'authorizing_user_id', - 'project_id', - 'requested_roles', - 'consumer_id', - 'expires_at']) - - def _assert_v4_later_tables(self): - self.assertTableColumns('consumer', - ['id', - 'description', - 'secret', - 'extra']) - self.assertTableColumns('request_token', - ['id', - 'request_secret', - 'verifier', - 'authorizing_user_id', - 'requested_project_id', - 'role_ids', - 'consumer_id', - 'expires_at']) - self.assertTableColumns('access_token', - ['id', - 'access_secret', - 'authorizing_user_id', - 'project_id', - 'role_ids', - 'consumer_id', - 'expires_at']) - def test_upgrade(self): - self.assertTableDoesNotExist('consumer') - self.assertTableDoesNotExist('request_token') - self.assertTableDoesNotExist('access_token') - self.upgrade(1) - self._assert_v1_3_tables() - - # NOTE(blk-u): Migrations 2-3 don't modify the tables in a way that we - # can easily test for. + for version in range(self.OAUTH1_MIGRATIONS): + v = version + 1 + self.assertRaises(exception.MigrationMovedFailure, + self.upgrade, version=v, + repository=self.repo_path) - self.upgrade(4) - self._assert_v4_later_tables() - self.upgrade(5) - self._assert_v4_later_tables() +class EndpointFilterExtension(test_sql_upgrade.SqlMigrateBase): + ENDPOINT_FILTER_MIGRATIONS = 2 -class EndpointFilterExtension(test_sql_upgrade.SqlMigrateBase): def repo_package(self): return endpoint_filter - def upgrade(self, version): - super(EndpointFilterExtension, self).upgrade( - version, repository=self.repo_path) - - def _assert_v1_tables(self): - self.assertTableColumns('project_endpoint', - ['endpoint_id', 'project_id']) - self.assertTableDoesNotExist('endpoint_group') - self.assertTableDoesNotExist('project_endpoint_group') - - def _assert_v2_tables(self): - self.assertTableColumns('project_endpoint', - ['endpoint_id', 'project_id']) - self.assertTableColumns('endpoint_group', - ['id', 'name', 'description', 'filters']) - self.assertTableColumns('project_endpoint_group', - ['endpoint_group_id', 'project_id']) - def test_upgrade(self): - self.assertTableDoesNotExist('project_endpoint') - self.upgrade(1) - self._assert_v1_tables() - self.assertTableColumns('project_endpoint', - ['endpoint_id', 'project_id']) - self.upgrade(2) - self._assert_v2_tables() + for version in range(self.ENDPOINT_FILTER_MIGRATIONS): + v = version + 1 + self.assertRaises(exception.MigrationMovedFailure, + self.upgrade, version=v, + repository=self.repo_path) class EndpointPolicyExtension(test_sql_upgrade.SqlMigrateBase): + + ENDPOINT_POLICY_MIGRATIONS = 1 + def repo_package(self): return endpoint_policy def test_upgrade(self): - self.assertTableDoesNotExist('policy_association') - self.upgrade(1, repository=self.repo_path) - self.assertTableColumns('policy_association', - ['id', 'policy_id', 'endpoint_id', - 'service_id', 'region_id']) + self.assertRaises(exception.MigrationMovedFailure, + self.upgrade, + version=self.ENDPOINT_POLICY_MIGRATIONS, + repository=self.repo_path) class FederationExtension(test_sql_upgrade.SqlMigrateBase): - """Test class for ensuring the Federation SQL.""" - def setUp(self): - super(FederationExtension, self).setUp() - self.identity_provider = 'identity_provider' - self.federation_protocol = 'federation_protocol' - self.service_provider = 'service_provider' - self.mapping = 'mapping' - self.remote_id_table = 'idp_remote_ids' + FEDERATION_MIGRATIONS = 8 def repo_package(self): return federation - def insert_dict(self, session, table_name, d): - """Naively inserts key-value pairs into a table, given a dictionary.""" - table = sqlalchemy.Table(table_name, self.metadata, autoload=True) - insert = table.insert().values(**d) - session.execute(insert) - session.commit() - def test_upgrade(self): - self.assertTableDoesNotExist(self.identity_provider) - self.assertTableDoesNotExist(self.federation_protocol) - self.assertTableDoesNotExist(self.mapping) - - self.upgrade(1, repository=self.repo_path) - self.assertTableColumns(self.identity_provider, - ['id', - 'enabled', - 'description']) - - self.assertTableColumns(self.federation_protocol, - ['id', - 'idp_id', - 'mapping_id']) - - self.upgrade(2, repository=self.repo_path) - self.assertTableColumns(self.mapping, - ['id', 'rules']) - - federation_protocol = utils.get_table( - self.engine, - 'federation_protocol') - with self.engine.begin() as conn: - conn.execute(federation_protocol.insert(), id=0, idp_id=1) - self.upgrade(3, repository=self.repo_path) - federation_protocol = utils.get_table( - self.engine, - 'federation_protocol') - self.assertFalse(federation_protocol.c.mapping_id.nullable) - - def test_service_provider_attributes_cannot_be_null(self): - self.upgrade(6, repository=self.repo_path) - self.assertTableColumns(self.service_provider, - ['id', 'description', 'enabled', 'auth_url', - 'sp_url']) - - session = self.Session() - sp1 = {'id': uuid.uuid4().hex, - 'auth_url': None, - 'sp_url': uuid.uuid4().hex, - 'description': uuid.uuid4().hex, - 'enabled': True} - sp2 = {'id': uuid.uuid4().hex, - 'auth_url': uuid.uuid4().hex, - 'sp_url': None, - 'description': uuid.uuid4().hex, - 'enabled': True} - sp3 = {'id': uuid.uuid4().hex, - 'auth_url': None, - 'sp_url': None, - 'description': uuid.uuid4().hex, - 'enabled': True} - - # Insert with 'auth_url' or 'sp_url' set to null must fail - self.assertRaises(db_exception.DBError, - self.insert_dict, - session, - self.service_provider, - sp1) - self.assertRaises(db_exception.DBError, - self.insert_dict, - session, - self.service_provider, - sp2) - self.assertRaises(db_exception.DBError, - self.insert_dict, - session, - self.service_provider, - sp3) - - session.close() - - def test_fixup_service_provider_attributes(self): - session = self.Session() - sp1 = {'id': uuid.uuid4().hex, - 'auth_url': None, - 'sp_url': uuid.uuid4().hex, - 'description': uuid.uuid4().hex, - 'enabled': True} - sp2 = {'id': uuid.uuid4().hex, - 'auth_url': uuid.uuid4().hex, - 'sp_url': None, - 'description': uuid.uuid4().hex, - 'enabled': True} - sp3 = {'id': uuid.uuid4().hex, - 'auth_url': None, - 'sp_url': None, - 'description': uuid.uuid4().hex, - 'enabled': True} - self.upgrade(5, repository=self.repo_path) - self.assertTableColumns(self.service_provider, - ['id', 'description', 'enabled', 'auth_url', - 'sp_url']) - - # Before the migration, the table should accept null values - self.insert_dict(session, self.service_provider, sp1) - self.insert_dict(session, self.service_provider, sp2) - self.insert_dict(session, self.service_provider, sp3) - - # Check if null values are updated to empty string when migrating - session.close() - self.upgrade(6, repository=self.repo_path) - sp_table = sqlalchemy.Table(self.service_provider, - self.metadata, - autoload=True) - session = self.Session() - self.metadata.clear() - - sp = session.query(sp_table).filter(sp_table.c.id == sp1['id'])[0] - self.assertEqual('', sp.auth_url) - - sp = session.query(sp_table).filter(sp_table.c.id == sp2['id'])[0] - self.assertEqual('', sp.sp_url) - - sp = session.query(sp_table).filter(sp_table.c.id == sp3['id'])[0] - self.assertEqual('', sp.auth_url) - self.assertEqual('', sp.sp_url) - - def test_propagate_remote_id_to_separate_column(self): - """Make sure empty remote_id is not propagated. - Test scenario: - - Upgrade database to version 6 where identity_provider table has a - remote_id column - - Add 3 identity provider objects, where idp1 and idp2 have valid - remote_id parameter set, and idp3 has it empty (None). - - Upgrade database to version 7 and expect migration scripts to - properly move data rom identity_provider.remote_id column into - separate table idp_remote_ids. - - In the idp_remote_ids table expect to find entries for idp1 and idp2 - and not find anything for idp3 (identitified by idp's id) - - """ - session = self.Session() - idp1 = {'id': uuid.uuid4().hex, - 'remote_id': uuid.uuid4().hex, - 'description': uuid.uuid4().hex, - 'enabled': True} - idp2 = {'id': uuid.uuid4().hex, - 'remote_id': uuid.uuid4().hex, - 'description': uuid.uuid4().hex, - 'enabled': True} - idp3 = {'id': uuid.uuid4().hex, - 'remote_id': None, - 'description': uuid.uuid4().hex, - 'enabled': True} - self.upgrade(6, repository=self.repo_path) - self.assertTableColumns(self.identity_provider, - ['id', 'description', 'enabled', 'remote_id']) - - self.insert_dict(session, self.identity_provider, idp1) - self.insert_dict(session, self.identity_provider, idp2) - self.insert_dict(session, self.identity_provider, idp3) - - session.close() - self.upgrade(7, repository=self.repo_path) - - self.assertTableColumns(self.identity_provider, - ['id', 'description', 'enabled']) - remote_id_table = sqlalchemy.Table(self.remote_id_table, - self.metadata, - autoload=True) - - session = self.Session() - self.metadata.clear() - - idp = session.query(remote_id_table).filter( - remote_id_table.c.idp_id == idp1['id'])[0] - self.assertEqual(idp1['remote_id'], idp.remote_id) - - idp = session.query(remote_id_table).filter( - remote_id_table.c.idp_id == idp2['id'])[0] - self.assertEqual(idp2['remote_id'], idp.remote_id) - - idp = session.query(remote_id_table).filter( - remote_id_table.c.idp_id == idp3['id']) - # NOTE(marek-denis): As idp3 had empty 'remote_id' attribute we expect - # not to find it in the 'remote_id_table' table, hence count should be - # 0.real - self.assertEqual(0, idp.count()) - - def test_add_relay_state_column(self): - self.upgrade(8, repository=self.repo_path) - self.assertTableColumns(self.service_provider, - ['id', 'description', 'enabled', 'auth_url', - 'relay_state_prefix', 'sp_url']) + for version in range(self.FEDERATION_MIGRATIONS): + v = version + 1 + self.assertRaises(exception.MigrationMovedFailure, + self.upgrade, version=v, + repository=self.repo_path) class RevokeExtension(test_sql_upgrade.SqlMigrateBase): - _REVOKE_COLUMN_NAMES = ['id', 'domain_id', 'project_id', 'user_id', - 'role_id', 'trust_id', 'consumer_id', - 'access_token_id', 'issued_before', 'expires_at', - 'revoked_at'] + REVOKE_MIGRATIONS = 2 def repo_package(self): return revoke def test_upgrade(self): - self.assertTableDoesNotExist('revocation_event') - self.upgrade(1, repository=self.repo_path) - self.assertTableColumns('revocation_event', - self._REVOKE_COLUMN_NAMES) + for version in range(self.REVOKE_MIGRATIONS): + v = version + 1 + self.assertRaises(exception.MigrationMovedFailure, + self.upgrade, version=v, + repository=self.repo_path) diff --git a/keystone-moon/keystone/tests/unit/test_sql_upgrade.py b/keystone-moon/keystone/tests/unit/test_sql_upgrade.py index d617d445..5ca12f66 100644 --- a/keystone-moon/keystone/tests/unit/test_sql_upgrade.py +++ b/keystone-moon/keystone/tests/unit/test_sql_upgrade.py @@ -29,11 +29,13 @@ WARNING:: all data will be lost. """ -import copy import json import uuid +import migrate from migrate.versioning import api as versioning_api +from migrate.versioning import repository +import mock from oslo_config import cfg from oslo_db import exception as db_exception from oslo_db.sqlalchemy import migration @@ -41,12 +43,10 @@ from oslo_db.sqlalchemy import session as db_session from sqlalchemy.engine import reflection import sqlalchemy.exc from sqlalchemy import schema +from testtools import matchers from keystone.common import sql -from keystone.common.sql import migrate_repo from keystone.common.sql import migration_helpers -from keystone.contrib import federation -from keystone.contrib import revoke from keystone import exception from keystone.tests import unit from keystone.tests.unit import default_fixtures @@ -54,7 +54,6 @@ from keystone.tests.unit.ksfixtures import database CONF = cfg.CONF -DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id # NOTE(morganfainberg): This should be updated when each DB migration collapse # is done to mirror the expected structure of the DB in the format of @@ -67,8 +66,8 @@ INITIAL_TABLE_STRUCTURE = { 'id', 'name', 'enabled', 'extra', ], 'endpoint': [ - 'id', 'legacy_endpoint_id', 'interface', 'region', 'service_id', 'url', - 'enabled', 'extra', + 'id', 'legacy_endpoint_id', 'interface', 'region_id', 'service_id', + 'url', 'enabled', 'extra', ], 'group': [ 'id', 'domain_id', 'name', 'description', 'extra', @@ -78,6 +77,7 @@ INITIAL_TABLE_STRUCTURE = { ], 'project': [ 'id', 'name', 'extra', 'description', 'enabled', 'domain_id', + 'parent_id', ], 'role': [ 'id', 'name', 'extra', @@ -108,23 +108,82 @@ INITIAL_TABLE_STRUCTURE = { 'assignment': [ 'type', 'actor_id', 'target_id', 'role_id', 'inherited', ], -} - - -INITIAL_EXTENSION_TABLE_STRUCTURE = { - 'revocation_event': [ - 'id', 'domain_id', 'project_id', 'user_id', 'role_id', - 'trust_id', 'consumer_id', 'access_token_id', - 'issued_before', 'expires_at', 'revoked_at', 'audit_id', - 'audit_chain_id', + 'id_mapping': [ + 'public_id', 'domain_id', 'local_id', 'entity_type', + ], + 'whitelisted_config': [ + 'domain_id', 'group', 'option', 'value', + ], + 'sensitive_config': [ + 'domain_id', 'group', 'option', 'value', ], } -EXTENSIONS = {'federation': federation, - 'revoke': revoke} + +# Test migration_helpers.get_init_version separately to ensure it works before +# using in the SqlUpgrade tests. +class MigrationHelpersGetInitVersionTests(unit.TestCase): + @mock.patch.object(repository, 'Repository') + def test_get_init_version_no_path(self, repo): + migrate_versions = mock.MagicMock() + # make a version list starting with zero. `get_init_version` will + # return None for this value. + migrate_versions.versions.versions = list(range(0, 5)) + repo.return_value = migrate_versions + + # os.path.isdir() is called by `find_migrate_repo()`. Mock it to avoid + # an exception. + with mock.patch('os.path.isdir', return_value=True): + # since 0 is the smallest version expect None + version = migration_helpers.get_init_version() + self.assertIsNone(version) + + # check that the default path was used as the first argument to the + # first invocation of repo. Cannot match the full path because it is + # based on where the test is run. + param = repo.call_args_list[0][0][0] + self.assertTrue(param.endswith('/sql/migrate_repo')) + + @mock.patch.object(repository, 'Repository') + def test_get_init_version_with_path_initial_version_0(self, repo): + migrate_versions = mock.MagicMock() + # make a version list starting with zero. `get_init_version` will + # return None for this value. + migrate_versions.versions.versions = list(range(0, 5)) + repo.return_value = migrate_versions + + # os.path.isdir() is called by `find_migrate_repo()`. Mock it to avoid + # an exception. + with mock.patch('os.path.isdir', return_value=True): + path = '/keystone/migrate_repo/' + + # since 0 is the smallest version expect None + version = migration_helpers.get_init_version(abs_path=path) + self.assertIsNone(version) + + @mock.patch.object(repository, 'Repository') + def test_get_init_version_with_path(self, repo): + initial_version = 10 + + migrate_versions = mock.MagicMock() + migrate_versions.versions.versions = list(range(initial_version + 1, + initial_version + 5)) + repo.return_value = migrate_versions + + # os.path.isdir() is called by `find_migrate_repo()`. Mock it to avoid + # an exception. + with mock.patch('os.path.isdir', return_value=True): + path = '/keystone/migrate_repo/' + + version = migration_helpers.get_init_version(abs_path=path) + self.assertEqual(initial_version, version) class SqlMigrateBase(unit.SQLDriverOverrides, unit.TestCase): + # override this in subclasses. The default of zero covers tests such + # as extensions upgrades. + _initial_db_version = 0 + def initialize_sql(self): self.metadata = sqlalchemy.MetaData() self.metadata.bind = self.engine @@ -139,6 +198,7 @@ class SqlMigrateBase(unit.SQLDriverOverrides, unit.TestCase): def setUp(self): super(SqlMigrateBase, self).setUp() + self.load_backends() database.initialize_sql_session() conn_str = CONF.database.connection if (conn_str != unit.IN_MEM_DB_CONN_STRING and @@ -155,7 +215,9 @@ class SqlMigrateBase(unit.SQLDriverOverrides, unit.TestCase): connection='sqlite:///%s' % db_file) # create and share a single sqlalchemy engine for testing - self.engine = sql.get_engine() + with sql.session_for_write() as session: + self.engine = session.get_bind() + self.addCleanup(self.cleanup_instance('engine')) self.Session = db_session.get_maker(self.engine, autocommit=False) self.addCleanup(sqlalchemy.orm.session.Session.close_all) @@ -164,7 +226,8 @@ class SqlMigrateBase(unit.SQLDriverOverrides, unit.TestCase): self.repo_package()) self.schema = versioning_api.ControlledSchema.create( self.engine, - self.repo_path, self.initial_db_version) + self.repo_path, + self._initial_db_version) # auto-detect the highest available schema version in the migrate_repo self.max_version = self.schema.repository.version().version @@ -229,6 +292,23 @@ class SqlMigrateBase(unit.SQLDriverOverrides, unit.TestCase): else: raise AssertionError('Table "%s" already exists' % table_name) + def assertTableCountsMatch(self, table1_name, table2_name): + try: + table1 = self.select_table(table1_name) + except sqlalchemy.exc.NoSuchTableError: + raise AssertionError('Table "%s" does not exist' % table1_name) + try: + table2 = self.select_table(table2_name) + except sqlalchemy.exc.NoSuchTableError: + raise AssertionError('Table "%s" does not exist' % table2_name) + session = self.Session() + table1_count = session.execute(table1.count()).scalar() + table2_count = session.execute(table2.count()).scalar() + if table1_count != table2_count: + raise AssertionError('Table counts do not match: {0} ({1}), {2} ' + '({3})'.format(table1_name, table1_count, + table2_name, table2_count)) + def upgrade(self, *args, **kwargs): self._migrate(*args, **kwargs) @@ -257,50 +337,30 @@ class SqlMigrateBase(unit.SQLDriverOverrides, unit.TestCase): self.assertItemsEqual(expected_cols, actual_cols, '%s table' % table_name) - @property - def initial_db_version(self): - return getattr(self, '_initial_db_version', 0) - class SqlUpgradeTests(SqlMigrateBase): - - _initial_db_version = migrate_repo.DB_INIT_VERSION + _initial_db_version = migration_helpers.get_init_version() def test_blank_db_to_start(self): self.assertTableDoesNotExist('user') def test_start_version_db_init_version(self): - version = migration.db_version(sql.get_engine(), self.repo_path, - migrate_repo.DB_INIT_VERSION) + with sql.session_for_write() as session: + version = migration.db_version(session.get_bind(), self.repo_path, + self._initial_db_version) self.assertEqual( - migrate_repo.DB_INIT_VERSION, + self._initial_db_version, version, - 'DB is not at version %s' % migrate_repo.DB_INIT_VERSION) + 'DB is not at version %s' % self._initial_db_version) def test_upgrade_add_initial_tables(self): - self.upgrade(migrate_repo.DB_INIT_VERSION + 1) + self.upgrade(self._initial_db_version + 1) self.check_initial_table_structure() def check_initial_table_structure(self): for table in INITIAL_TABLE_STRUCTURE: self.assertTableColumns(table, INITIAL_TABLE_STRUCTURE[table]) - # Ensure the default domain was properly created. - default_domain = migration_helpers.get_default_domain() - - meta = sqlalchemy.MetaData() - meta.bind = self.engine - - domain_table = sqlalchemy.Table('domain', meta, autoload=True) - - session = self.Session() - q = session.query(domain_table) - refs = q.all() - - self.assertEqual(1, len(refs)) - for k in default_domain.keys(): - self.assertEqual(default_domain[k], getattr(refs[0], k)) - def insert_dict(self, session, table_name, d, table=None): """Naively inserts key-value pairs into a table, given a dictionary.""" if table is None: @@ -312,127 +372,43 @@ class SqlUpgradeTests(SqlMigrateBase): session.execute(insert) session.commit() - def test_id_mapping(self): - self.upgrade(50) - self.assertTableDoesNotExist('id_mapping') - self.upgrade(51) - self.assertTableExists('id_mapping') - - def test_region_url_upgrade(self): - self.upgrade(52) - self.assertTableColumns('region', - ['id', 'description', 'parent_region_id', - 'extra', 'url']) - - def test_endpoint_region_upgrade_columns(self): - self.upgrade(53) - self.assertTableColumns('endpoint', - ['id', 'legacy_endpoint_id', 'interface', - 'service_id', 'url', 'extra', 'enabled', - 'region_id']) - region_table = sqlalchemy.Table('region', self.metadata, autoload=True) - self.assertEqual(255, region_table.c.id.type.length) - self.assertEqual(255, region_table.c.parent_region_id.type.length) - endpoint_table = sqlalchemy.Table('endpoint', - self.metadata, - autoload=True) - self.assertEqual(255, endpoint_table.c.region_id.type.length) - - def test_endpoint_region_migration(self): - self.upgrade(52) - session = self.Session() - _small_region_name = '0' * 30 - _long_region_name = '0' * 255 - _clashing_region_name = '0' * 70 - - def add_service(): - service_id = uuid.uuid4().hex - - service = { - 'id': service_id, - 'type': uuid.uuid4().hex - } - - self.insert_dict(session, 'service', service) - - return service_id - - def add_endpoint(service_id, region): - endpoint_id = uuid.uuid4().hex - - endpoint = { - 'id': endpoint_id, - 'interface': uuid.uuid4().hex[:8], - 'service_id': service_id, - 'url': uuid.uuid4().hex, - 'region': region - } - self.insert_dict(session, 'endpoint', endpoint) - - return endpoint_id - - _service_id_ = add_service() - add_endpoint(_service_id_, region=_long_region_name) - add_endpoint(_service_id_, region=_long_region_name) - add_endpoint(_service_id_, region=_clashing_region_name) - add_endpoint(_service_id_, region=_small_region_name) - add_endpoint(_service_id_, region=None) - - # upgrade to 53 - session.close() - self.upgrade(53) - session = self.Session() - self.metadata.clear() + def test_kilo_squash(self): + self.upgrade(67) - region_table = sqlalchemy.Table('region', self.metadata, autoload=True) - self.assertEqual(1, session.query(region_table). - filter_by(id=_long_region_name).count()) - self.assertEqual(1, session.query(region_table). - filter_by(id=_clashing_region_name).count()) - self.assertEqual(1, session.query(region_table). - filter_by(id=_small_region_name).count()) + # In 053 the size of ID and parent region ID columns were changed + table = sqlalchemy.Table('region', self.metadata, autoload=True) + self.assertEqual(255, table.c.id.type.length) + self.assertEqual(255, table.c.parent_region_id.type.length) + table = sqlalchemy.Table('endpoint', self.metadata, autoload=True) + self.assertEqual(255, table.c.region_id.type.length) - endpoint_table = sqlalchemy.Table('endpoint', - self.metadata, - autoload=True) - self.assertEqual(5, session.query(endpoint_table).count()) - self.assertEqual(2, session.query(endpoint_table). - filter_by(region_id=_long_region_name).count()) - self.assertEqual(1, session.query(endpoint_table). - filter_by(region_id=_clashing_region_name).count()) - self.assertEqual(1, session.query(endpoint_table). - filter_by(region_id=_small_region_name).count()) - - def test_add_actor_id_index(self): - self.upgrade(53) - self.upgrade(54) + # In 054 an index was created for the actor_id of the assignment table table = sqlalchemy.Table('assignment', self.metadata, autoload=True) index_data = [(idx.name, list(idx.columns.keys())) for idx in table.indexes] self.assertIn(('ix_actor_id', ['actor_id']), index_data) - def test_token_user_id_and_trust_id_index_upgrade(self): - self.upgrade(54) - self.upgrade(55) + # In 055 indexes were created for user and trust IDs in the token table table = sqlalchemy.Table('token', self.metadata, autoload=True) index_data = [(idx.name, list(idx.columns.keys())) for idx in table.indexes] self.assertIn(('ix_token_user_id', ['user_id']), index_data) self.assertIn(('ix_token_trust_id', ['trust_id']), index_data) - def test_project_parent_id_upgrade(self): - self.upgrade(61) - self.assertTableColumns('project', - ['id', 'name', 'extra', 'description', - 'enabled', 'domain_id', 'parent_id']) + # In 062 the role ID foreign key was removed from the assignment table + if self.engine.name == "mysql": + self.assertFalse(self.does_fk_exist('assignment', 'role_id')) - def test_drop_assignment_role_fk(self): - self.upgrade(61) - self.assertTrue(self.does_fk_exist('assignment', 'role_id')) - self.upgrade(62) + # In 064 the domain ID FK was removed from the group and user tables if self.engine.name != 'sqlite': # sqlite does not support FK deletions (or enforcement) - self.assertFalse(self.does_fk_exist('assignment', 'role_id')) + self.assertFalse(self.does_fk_exist('group', 'domain_id')) + self.assertFalse(self.does_fk_exist('user', 'domain_id')) + + # In 067 the role ID index was removed from the assignment table + if self.engine.name == "mysql": + self.assertFalse(self._does_index_exist('assignment', + 'assignment_role_id_fkey')) def test_insert_assignment_inherited_pk(self): ASSIGNMENT_TABLE_NAME = 'assignment' @@ -502,7 +478,6 @@ class SqlUpgradeTests(SqlMigrateBase): def does_pk_exist(self, table, pk_column): """Checks whether a column is primary key on a table.""" - inspector = reflection.Inspector.from_engine(self.engine) pk_columns = inspector.get_pk_constraint(table)['constrained_columns'] @@ -515,119 +490,164 @@ class SqlUpgradeTests(SqlMigrateBase): return True return False - def test_drop_region_url_upgrade(self): - self.upgrade(63) - self.assertTableColumns('region', - ['id', 'description', 'parent_region_id', - 'extra']) - - def test_domain_fk(self): - self.upgrade(63) - self.assertTrue(self.does_fk_exist('group', 'domain_id')) - self.assertTrue(self.does_fk_exist('user', 'domain_id')) - self.upgrade(64) - if self.engine.name != 'sqlite': - # sqlite does not support FK deletions (or enforcement) - self.assertFalse(self.does_fk_exist('group', 'domain_id')) - self.assertFalse(self.does_fk_exist('user', 'domain_id')) - - def test_add_domain_config(self): - whitelisted_table = 'whitelisted_config' - sensitive_table = 'sensitive_config' - self.upgrade(64) - self.assertTableDoesNotExist(whitelisted_table) - self.assertTableDoesNotExist(sensitive_table) - self.upgrade(65) - self.assertTableColumns(whitelisted_table, - ['domain_id', 'group', 'option', 'value']) - self.assertTableColumns(sensitive_table, - ['domain_id', 'group', 'option', 'value']) - - def test_fixup_service_name_value_upgrade(self): - """Update service name data from `extra` to empty string.""" - def add_service(**extra_data): - service_id = uuid.uuid4().hex - - service = { - 'id': service_id, - 'type': uuid.uuid4().hex, - 'extra': json.dumps(extra_data), - } - - self.insert_dict(session, 'service', service) - - return service_id - - self.upgrade(65) - session = self.Session() - - # Services with extra values having a random attribute and - # different combinations of name - random_attr_name = uuid.uuid4().hex - random_attr_value = uuid.uuid4().hex - random_attr_str = "%s='%s'" % (random_attr_name, random_attr_value) - random_attr_no_name = {random_attr_name: random_attr_value} - random_attr_no_name_str = "%s='%s'" % (random_attr_name, - random_attr_value) - random_attr_name_value = {random_attr_name: random_attr_value, - 'name': 'myname'} - random_attr_name_value_str = 'name=myname,%s' % random_attr_str - random_attr_name_empty = {random_attr_name: random_attr_value, - 'name': ''} - random_attr_name_empty_str = 'name=,%s' % random_attr_str - random_attr_name_none = {random_attr_name: random_attr_value, - 'name': None} - random_attr_name_none_str = 'name=None,%s' % random_attr_str - - services = [ - (add_service(**random_attr_no_name), - random_attr_name_empty, random_attr_no_name_str), - (add_service(**random_attr_name_value), - random_attr_name_value, random_attr_name_value_str), - (add_service(**random_attr_name_empty), - random_attr_name_empty, random_attr_name_empty_str), - (add_service(**random_attr_name_none), - random_attr_name_empty, random_attr_name_none_str), - ] - - # NOTE(viktors): Add a service with empty extra field - self.insert_dict(session, 'service', - {'id': uuid.uuid4().hex, 'type': uuid.uuid4().hex}) - - session.close() - self.upgrade(66) - session = self.Session() - - # Verify that the services have the expected values. - self.metadata.clear() - service_table = sqlalchemy.Table('service', self.metadata, - autoload=True) - - def fetch_service_extra(service_id): - cols = [service_table.c.extra] - f = service_table.c.id == service_id - s = sqlalchemy.select(cols).where(f) - service = session.execute(s).fetchone() - return json.loads(service.extra) - - for service_id, exp_extra, msg in services: - extra = fetch_service_extra(service_id) - self.assertDictEqual(exp_extra, extra, msg) - - def _does_index_exist(self, table_name, index_name): + def does_index_exist(self, table_name, index_name): meta = sqlalchemy.MetaData(bind=self.engine) - table = sqlalchemy.Table('assignment', meta, autoload=True) + table = sqlalchemy.Table(table_name, meta, autoload=True) return index_name in [idx.name for idx in table.indexes] - def test_drop_assignment_role_id_index_mysql(self): - self.upgrade(66) - if self.engine.name == "mysql": - self.assertTrue(self._does_index_exist('assignment', - 'assignment_role_id_fkey')) - self.upgrade(67) - if self.engine.name == "mysql": - self.assertFalse(self._does_index_exist('assignment', - 'assignment_role_id_fkey')) + def does_constraint_exist(self, table_name, constraint_name): + meta = sqlalchemy.MetaData(bind=self.engine) + table = sqlalchemy.Table(table_name, meta, autoload=True) + return constraint_name in [con.name for con in table.constraints] + + def test_endpoint_policy_upgrade(self): + self.assertTableDoesNotExist('policy_association') + self.upgrade(81) + self.assertTableColumns('policy_association', + ['id', 'policy_id', 'endpoint_id', + 'service_id', 'region_id']) + + @mock.patch.object(migration_helpers, 'get_db_version', return_value=1) + def test_endpoint_policy_already_migrated(self, mock_ep): + + # By setting the return value to 1, the migration has already been + # run, and there's no need to create the table again + + self.upgrade(81) + + mock_ep.assert_called_once_with(extension='endpoint_policy', + engine=mock.ANY) + + # It won't exist because we are mocking it, but we can verify + # that 081 did not create the table + self.assertTableDoesNotExist('policy_association') + + def test_create_federation_tables(self): + self.identity_provider = 'identity_provider' + self.federation_protocol = 'federation_protocol' + self.service_provider = 'service_provider' + self.mapping = 'mapping' + self.remote_ids = 'idp_remote_ids' + + self.assertTableDoesNotExist(self.identity_provider) + self.assertTableDoesNotExist(self.federation_protocol) + self.assertTableDoesNotExist(self.service_provider) + self.assertTableDoesNotExist(self.mapping) + self.assertTableDoesNotExist(self.remote_ids) + + self.upgrade(82) + self.assertTableColumns(self.identity_provider, + ['id', 'description', 'enabled']) + + self.assertTableColumns(self.federation_protocol, + ['id', 'idp_id', 'mapping_id']) + + self.assertTableColumns(self.mapping, + ['id', 'rules']) + + self.assertTableColumns(self.service_provider, + ['id', 'description', 'enabled', 'auth_url', + 'relay_state_prefix', 'sp_url']) + + self.assertTableColumns(self.remote_ids, ['idp_id', 'remote_id']) + + federation_protocol = sqlalchemy.Table(self.federation_protocol, + self.metadata, + autoload=True) + self.assertFalse(federation_protocol.c.mapping_id.nullable) + + sp_table = sqlalchemy.Table(self.service_provider, + self.metadata, + autoload=True) + self.assertFalse(sp_table.c.auth_url.nullable) + self.assertFalse(sp_table.c.sp_url.nullable) + + @mock.patch.object(migration_helpers, 'get_db_version', return_value=8) + def test_federation_already_migrated(self, mock_federation): + + # By setting the return value to 8, the migration has already been + # run, and there's no need to create the table again. + self.upgrade(82) + + mock_federation.assert_any_call(extension='federation', + engine=mock.ANY) + + # It won't exist because we are mocking it, but we can verify + # that 082 did not create the table. + self.assertTableDoesNotExist('identity_provider') + self.assertTableDoesNotExist('federation_protocol') + self.assertTableDoesNotExist('mapping') + self.assertTableDoesNotExist('service_provider') + self.assertTableDoesNotExist('idp_remote_ids') + + def test_create_oauth_tables(self): + consumer = 'consumer' + request_token = 'request_token' + access_token = 'access_token' + self.assertTableDoesNotExist(consumer) + self.assertTableDoesNotExist(request_token) + self.assertTableDoesNotExist(access_token) + self.upgrade(83) + self.assertTableColumns(consumer, + ['id', + 'description', + 'secret', + 'extra']) + self.assertTableColumns(request_token, + ['id', + 'request_secret', + 'verifier', + 'authorizing_user_id', + 'requested_project_id', + 'role_ids', + 'consumer_id', + 'expires_at']) + self.assertTableColumns(access_token, + ['id', + 'access_secret', + 'authorizing_user_id', + 'project_id', + 'role_ids', + 'consumer_id', + 'expires_at']) + + @mock.patch.object(migration_helpers, 'get_db_version', return_value=5) + def test_oauth1_already_migrated(self, mock_oauth1): + + # By setting the return value to 5, the migration has already been + # run, and there's no need to create the table again. + self.upgrade(83) + + mock_oauth1.assert_any_call(extension='oauth1', engine=mock.ANY) + + # It won't exist because we are mocking it, but we can verify + # that 083 did not create the table. + self.assertTableDoesNotExist('consumer') + self.assertTableDoesNotExist('request_token') + self.assertTableDoesNotExist('access_token') + + def test_create_revoke_table(self): + self.assertTableDoesNotExist('revocation_event') + self.upgrade(84) + self.assertTableColumns('revocation_event', + ['id', 'domain_id', 'project_id', 'user_id', + 'role_id', 'trust_id', 'consumer_id', + 'access_token_id', 'issued_before', + 'expires_at', 'revoked_at', + 'audit_chain_id', 'audit_id']) + + @mock.patch.object(migration_helpers, 'get_db_version', return_value=2) + def test_revoke_already_migrated(self, mock_revoke): + + # By setting the return value to 2, the migration has already been + # run, and there's no need to create the table again. + self.upgrade(84) + + mock_revoke.assert_any_call(extension='revoke', engine=mock.ANY) + + # It won't exist because we are mocking it, but we can verify + # that 084 did not create the table. + self.assertTableDoesNotExist('revocation_event') def test_project_is_domain_upgrade(self): self.upgrade(74) @@ -636,6 +656,13 @@ class SqlUpgradeTests(SqlMigrateBase): 'enabled', 'domain_id', 'parent_id', 'is_domain']) + def test_implied_roles_upgrade(self): + self.upgrade(87) + self.assertTableColumns('implied_role', + ['prior_role_id', 'implied_role_id']) + self.assertTrue(self.does_fk_exist('implied_role', 'prior_role_id')) + self.assertTrue(self.does_fk_exist('implied_role', 'implied_role_id')) + def test_add_config_registration(self): config_registration = 'config_register' self.upgrade(74) @@ -643,136 +670,497 @@ class SqlUpgradeTests(SqlMigrateBase): self.upgrade(75) self.assertTableColumns(config_registration, ['type', 'domain_id']) - def populate_user_table(self, with_pass_enab=False, - with_pass_enab_domain=False): - # Populate the appropriate fields in the user - # table, depending on the parameters: - # - # Default: id, name, extra - # pass_enab: Add password, enabled as well - # pass_enab_domain: Add password, enabled and domain as well - # - this_table = sqlalchemy.Table("user", - self.metadata, - autoload=True) - for user in default_fixtures.USERS: - extra = copy.deepcopy(user) - extra.pop('id') - extra.pop('name') - - if with_pass_enab: - password = extra.pop('password', None) - enabled = extra.pop('enabled', True) - ins = this_table.insert().values( + def test_endpoint_filter_upgrade(self): + def assert_tables_columns_exist(): + self.assertTableColumns('project_endpoint', + ['endpoint_id', 'project_id']) + self.assertTableColumns('endpoint_group', + ['id', 'name', 'description', 'filters']) + self.assertTableColumns('project_endpoint_group', + ['endpoint_group_id', 'project_id']) + + self.assertTableDoesNotExist('project_endpoint') + self.upgrade(85) + assert_tables_columns_exist() + + @mock.patch.object(migration_helpers, 'get_db_version', return_value=2) + def test_endpoint_filter_already_migrated(self, mock_endpoint_filter): + + # By setting the return value to 2, the migration has already been + # run, and there's no need to create the table again. + self.upgrade(85) + + mock_endpoint_filter.assert_any_call(extension='endpoint_filter', + engine=mock.ANY) + + # It won't exist because we are mocking it, but we can verify + # that 085 did not create the table. + self.assertTableDoesNotExist('project_endpoint') + self.assertTableDoesNotExist('endpoint_group') + self.assertTableDoesNotExist('project_endpoint_group') + + def test_add_trust_unique_constraint_upgrade(self): + self.upgrade(86) + inspector = reflection.Inspector.from_engine(self.engine) + constraints = inspector.get_unique_constraints('trust') + constraint_names = [constraint['name'] for constraint in constraints] + self.assertIn('duplicate_trust_constraint', constraint_names) + + def test_add_domain_specific_roles(self): + """Check database upgraded successfully for domain specific roles. + + The following items need to be checked: + + - The domain_id column has been added + - That it has been added to the uniqueness constraints + - Existing roles have their domain_id columns set to the specific + string of '<>' + + """ + NULL_DOMAIN_ID = '<>' + + self.upgrade(87) + session = self.Session() + role_table = sqlalchemy.Table('role', self.metadata, autoload=True) + # Add a role before we upgrade, so we can check that its new domain_id + # attribute is handled correctly + role_id = uuid.uuid4().hex + self.insert_dict(session, 'role', + {'id': role_id, 'name': uuid.uuid4().hex}) + session.close() + + self.upgrade(88) + + session = self.Session() + self.metadata.clear() + self.assertTableColumns('role', ['id', 'name', 'domain_id', 'extra']) + # Check the domain_id has been added to the uniqueness constraint + inspector = reflection.Inspector.from_engine(self.engine) + constraints = inspector.get_unique_constraints('role') + constraint_columns = [ + constraint['column_names'] for constraint in constraints + if constraint['name'] == 'ixu_role_name_domain_id'] + self.assertIn('domain_id', constraint_columns[0]) + + # Now check our role has its domain_id attribute set correctly + role_table = sqlalchemy.Table('role', self.metadata, autoload=True) + cols = [role_table.c.domain_id] + filter = role_table.c.id == role_id + statement = sqlalchemy.select(cols).where(filter) + role_entry = session.execute(statement).fetchone() + self.assertEqual(NULL_DOMAIN_ID, role_entry[0]) + + def test_add_root_of_all_domains(self): + NULL_DOMAIN_ID = '<>' + self.upgrade(89) + session = self.Session() + + domain_table = sqlalchemy.Table( + 'domain', self.metadata, autoload=True) + query = session.query(domain_table).filter_by(id=NULL_DOMAIN_ID) + domain_from_db = query.one() + self.assertIn(NULL_DOMAIN_ID, domain_from_db) + + project_table = sqlalchemy.Table( + 'project', self.metadata, autoload=True) + query = session.query(project_table).filter_by(id=NULL_DOMAIN_ID) + project_from_db = query.one() + self.assertIn(NULL_DOMAIN_ID, project_from_db) + + session.close() + + def test_add_local_user_and_password_tables(self): + local_user_table = 'local_user' + password_table = 'password' + self.upgrade(89) + self.assertTableDoesNotExist(local_user_table) + self.assertTableDoesNotExist(password_table) + self.upgrade(90) + self.assertTableColumns(local_user_table, + ['id', + 'user_id', + 'domain_id', + 'name']) + self.assertTableColumns(password_table, + ['id', + 'local_user_id', + 'password']) + + def test_migrate_data_to_local_user_and_password_tables(self): + def get_expected_users(): + expected_users = [] + for test_user in default_fixtures.USERS: + user = {} + user['id'] = uuid.uuid4().hex + user['name'] = test_user['name'] + user['domain_id'] = test_user['domain_id'] + user['password'] = test_user['password'] + user['enabled'] = True + user['extra'] = json.dumps(uuid.uuid4().hex) + user['default_project_id'] = uuid.uuid4().hex + expected_users.append(user) + return expected_users + + def add_users_to_db(expected_users, user_table): + for user in expected_users: + ins = user_table.insert().values( {'id': user['id'], 'name': user['name'], - 'password': password, - 'enabled': bool(enabled), - 'extra': json.dumps(extra)}) - else: - if with_pass_enab_domain: - password = extra.pop('password', None) - enabled = extra.pop('enabled', True) - extra.pop('domain_id') - ins = this_table.insert().values( - {'id': user['id'], - 'name': user['name'], - 'domain_id': user['domain_id'], - 'password': password, - 'enabled': bool(enabled), - 'extra': json.dumps(extra)}) - else: - ins = this_table.insert().values( - {'id': user['id'], - 'name': user['name'], - 'extra': json.dumps(extra)}) - self.engine.execute(ins) - - def populate_tenant_table(self, with_desc_enab=False, - with_desc_enab_domain=False): - # Populate the appropriate fields in the tenant or - # project table, depending on the parameters - # - # Default: id, name, extra - # desc_enab: Add description, enabled as well - # desc_enab_domain: Add description, enabled and domain as well, - # plus use project instead of tenant - # - if with_desc_enab_domain: - # By this time tenants are now projects - this_table = sqlalchemy.Table("project", - self.metadata, + 'domain_id': user['domain_id'], + 'password': user['password'], + 'enabled': user['enabled'], + 'extra': user['extra'], + 'default_project_id': user['default_project_id']}) + ins.execute() + + def get_users_from_db(user_table, local_user_table, password_table): + sel = ( + sqlalchemy.select([user_table.c.id, + user_table.c.enabled, + user_table.c.extra, + user_table.c.default_project_id, + local_user_table.c.name, + local_user_table.c.domain_id, + password_table.c.password]) + .select_from(user_table.join(local_user_table, + user_table.c.id == + local_user_table.c.user_id) + .join(password_table, + local_user_table.c.id == + password_table.c.local_user_id)) + ) + user_rows = sel.execute() + users = [] + for row in user_rows: + users.append( + {'id': row['id'], + 'name': row['name'], + 'domain_id': row['domain_id'], + 'password': row['password'], + 'enabled': row['enabled'], + 'extra': row['extra'], + 'default_project_id': row['default_project_id']}) + return users + + meta = sqlalchemy.MetaData() + meta.bind = self.engine + + user_table_name = 'user' + local_user_table_name = 'local_user' + password_table_name = 'password' + + # populate current user table + self.upgrade(90) + user_table = sqlalchemy.Table(user_table_name, meta, autoload=True) + expected_users = get_expected_users() + add_users_to_db(expected_users, user_table) + + # upgrade to migration and test + self.upgrade(91) + self.assertTableCountsMatch(user_table_name, local_user_table_name) + self.assertTableCountsMatch(local_user_table_name, password_table_name) + meta.clear() + user_table = sqlalchemy.Table(user_table_name, meta, autoload=True) + local_user_table = sqlalchemy.Table(local_user_table_name, meta, + autoload=True) + password_table = sqlalchemy.Table(password_table_name, meta, autoload=True) + actual_users = get_users_from_db(user_table, local_user_table, + password_table) + self.assertListEqual(expected_users, actual_users) + + def test_migrate_user_with_null_password_to_password_tables(self): + USER_TABLE_NAME = 'user' + LOCAL_USER_TABLE_NAME = 'local_user' + PASSWORD_TABLE_NAME = 'password' + self.upgrade(90) + user_ref = unit.new_user_ref(uuid.uuid4().hex) + user_ref.pop('password') + # pop extra attribute which doesn't recognized by SQL expression + # layer. + user_ref.pop('email') + session = self.Session() + self.insert_dict(session, USER_TABLE_NAME, user_ref) + self.metadata.clear() + self.upgrade(91) + # migration should be successful. + self.assertTableCountsMatch(USER_TABLE_NAME, LOCAL_USER_TABLE_NAME) + # no new entry was added to the password table because the + # user doesn't have a password. + password_table = self.select_table(PASSWORD_TABLE_NAME) + rows = session.execute(password_table.count()).scalar() + self.assertEqual(0, rows) + + def test_migrate_user_skip_user_already_exist_in_local_user(self): + USER_TABLE_NAME = 'user' + LOCAL_USER_TABLE_NAME = 'local_user' + self.upgrade(90) + user1_ref = unit.new_user_ref(uuid.uuid4().hex) + # pop extra attribute which doesn't recognized by SQL expression + # layer. + user1_ref.pop('email') + user2_ref = unit.new_user_ref(uuid.uuid4().hex) + user2_ref.pop('email') + session = self.Session() + self.insert_dict(session, USER_TABLE_NAME, user1_ref) + self.insert_dict(session, USER_TABLE_NAME, user2_ref) + user_id = user1_ref.pop('id') + user_name = user1_ref.pop('name') + domain_id = user1_ref.pop('domain_id') + local_user_ref = {'user_id': user_id, 'name': user_name, + 'domain_id': domain_id} + self.insert_dict(session, LOCAL_USER_TABLE_NAME, local_user_ref) + self.metadata.clear() + self.upgrade(91) + # migration should be successful and user2_ref has been migrated to + # `local_user` table. + self.assertTableCountsMatch(USER_TABLE_NAME, LOCAL_USER_TABLE_NAME) + + def test_implied_roles_fk_on_delete_cascade(self): + if self.engine.name == 'sqlite': + self.skipTest('sqlite backend does not support foreign keys') + + self.upgrade(92) + + def _create_three_roles(): + id_list = [] + for _ in range(3): + role = unit.new_role_ref() + self.role_api.create_role(role['id'], role) + id_list.append(role['id']) + return id_list + + role_id_list = _create_three_roles() + self.role_api.create_implied_role(role_id_list[0], role_id_list[1]) + self.role_api.create_implied_role(role_id_list[0], role_id_list[2]) + + # assert that there are two roles implied by role 0. + implied_roles = self.role_api.list_implied_roles(role_id_list[0]) + self.assertThat(implied_roles, matchers.HasLength(2)) + + self.role_api.delete_role(role_id_list[0]) + # assert the cascade deletion is effective. + implied_roles = self.role_api.list_implied_roles(role_id_list[0]) + self.assertThat(implied_roles, matchers.HasLength(0)) + + def test_domain_as_project_upgrade(self): + + def _populate_domain_and_project_tables(session): + # Three domains, with various different attributes + self.domains = [{'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'enabled': True, + 'extra': {'description': uuid.uuid4().hex, + 'another_attribute': True}}, + {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'enabled': True, + 'extra': {'description': uuid.uuid4().hex}}, + {'id': uuid.uuid4().hex, + 'name': uuid.uuid4().hex, + 'enabled': False}] + # Four projects, two top level, two children + self.projects = [] + self.projects.append(unit.new_project_ref( + domain_id=self.domains[0]['id'], + parent_id=None)) + self.projects.append(unit.new_project_ref( + domain_id=self.domains[0]['id'], + parent_id=self.projects[0]['id'])) + self.projects.append(unit.new_project_ref( + domain_id=self.domains[1]['id'], + parent_id=None)) + self.projects.append(unit.new_project_ref( + domain_id=self.domains[1]['id'], + parent_id=self.projects[2]['id'])) + + for domain in self.domains: + this_domain = domain.copy() + if 'extra' in this_domain: + this_domain['extra'] = json.dumps(this_domain['extra']) + self.insert_dict(session, 'domain', this_domain) + for project in self.projects: + self.insert_dict(session, 'project', project) + + def _check_projects(projects): + + def _assert_domain_matches_project(project): + for domain in self.domains: + if project.id == domain['id']: + self.assertEqual(domain['name'], project.name) + self.assertEqual(domain['enabled'], project.enabled) + if domain['id'] == self.domains[0]['id']: + self.assertEqual(domain['extra']['description'], + project.description) + self.assertEqual({'another_attribute': True}, + json.loads(project.extra)) + elif domain['id'] == self.domains[1]['id']: + self.assertEqual(domain['extra']['description'], + project.description) + self.assertEqual({}, json.loads(project.extra)) + + # We had domains 3 we created, which should now be projects acting + # as domains, To this we add the 4 original projects, plus the root + # of all domains row. + self.assertEqual(8, projects.count()) + + project_ids = [] + for project in projects: + if project.is_domain: + self.assertEqual(NULL_DOMAIN_ID, project.domain_id) + self.assertIsNone(project.parent_id) + else: + self.assertIsNotNone(project.domain_id) + self.assertIsNotNone(project.parent_id) + project_ids.append(project.id) + + for domain in self.domains: + self.assertIn(domain['id'], project_ids) + for project in self.projects: + self.assertIn(project['id'], project_ids) + + # Now check the attributes of the domains came across OK + for project in projects: + _assert_domain_matches_project(project) + + NULL_DOMAIN_ID = '<>' + self.upgrade(92) + + session = self.Session() + + _populate_domain_and_project_tables(session) + + self.upgrade(93) + proj_table = sqlalchemy.Table('project', self.metadata, autoload=True) + + projects = session.query(proj_table) + _check_projects(projects) + + def test_add_federated_user_table(self): + federated_user_table = 'federated_user' + self.upgrade(93) + self.assertTableDoesNotExist(federated_user_table) + self.upgrade(94) + self.assertTableColumns(federated_user_table, + ['id', + 'user_id', + 'idp_id', + 'protocol_id', + 'unique_id', + 'display_name']) + + def test_add_int_pkey_to_revocation_event_table(self): + meta = sqlalchemy.MetaData() + meta.bind = self.engine + REVOCATION_EVENT_TABLE_NAME = 'revocation_event' + self.upgrade(94) + revocation_event_table = sqlalchemy.Table(REVOCATION_EVENT_TABLE_NAME, + meta, autoload=True) + # assert id column is a string (before) + self.assertEqual('VARCHAR(64)', str(revocation_event_table.c.id.type)) + self.upgrade(95) + meta.clear() + revocation_event_table = sqlalchemy.Table(REVOCATION_EVENT_TABLE_NAME, + meta, autoload=True) + # assert id column is an integer (after) + self.assertEqual('INTEGER', str(revocation_event_table.c.id.type)) + + def _add_unique_constraint_to_role_name(self, + constraint_name='ixu_role_name'): + meta = sqlalchemy.MetaData() + meta.bind = self.engine + role_table = sqlalchemy.Table('role', meta, autoload=True) + migrate.UniqueConstraint(role_table.c.name, + name=constraint_name).create() + + def _drop_unique_constraint_to_role_name(self, + constraint_name='ixu_role_name'): + role_table = sqlalchemy.Table('role', self.metadata, autoload=True) + migrate.UniqueConstraint(role_table.c.name, + name=constraint_name).drop() + + def test_migration_88_drops_unique_constraint(self): + self.upgrade(87) + if self.engine.name == 'mysql': + self.assertTrue(self.does_index_exist('role', 'ixu_role_name')) else: - this_table = sqlalchemy.Table("tenant", - self.metadata, - autoload=True) + self.assertTrue(self.does_constraint_exist('role', + 'ixu_role_name')) + self.upgrade(88) + if self.engine.name == 'mysql': + self.assertFalse(self.does_index_exist('role', 'ixu_role_name')) + else: + self.assertFalse(self.does_constraint_exist('role', + 'ixu_role_name')) - for tenant in default_fixtures.TENANTS: - extra = copy.deepcopy(tenant) - extra.pop('id') - extra.pop('name') - - if with_desc_enab: - desc = extra.pop('description', None) - enabled = extra.pop('enabled', True) - ins = this_table.insert().values( - {'id': tenant['id'], - 'name': tenant['name'], - 'description': desc, - 'enabled': bool(enabled), - 'extra': json.dumps(extra)}) - else: - if with_desc_enab_domain: - desc = extra.pop('description', None) - enabled = extra.pop('enabled', True) - extra.pop('domain_id') - ins = this_table.insert().values( - {'id': tenant['id'], - 'name': tenant['name'], - 'domain_id': tenant['domain_id'], - 'description': desc, - 'enabled': bool(enabled), - 'extra': json.dumps(extra)}) - else: - ins = this_table.insert().values( - {'id': tenant['id'], - 'name': tenant['name'], - 'extra': json.dumps(extra)}) - self.engine.execute(ins) - - def _mysql_check_all_tables_innodb(self): - database = self.engine.url.database - - connection = self.engine.connect() - # sanity check - total = connection.execute("SELECT count(*) " - "from information_schema.TABLES " - "where TABLE_SCHEMA='%(database)s'" % - dict(database=database)) - self.assertTrue(total.scalar() > 0, "No tables found. Wrong schema?") - - noninnodb = connection.execute("SELECT table_name " - "from information_schema.TABLES " - "where TABLE_SCHEMA='%(database)s' " - "and ENGINE!='InnoDB' " - "and TABLE_NAME!='migrate_version'" % - dict(database=database)) - names = [x[0] for x in noninnodb] - self.assertEqual([], names, - "Non-InnoDB tables exist") - - connection.close() + def test_migration_88_inconsistent_constraint_name(self): + self.upgrade(87) + self._drop_unique_constraint_to_role_name() + + constraint_name = uuid.uuid4().hex + self._add_unique_constraint_to_role_name( + constraint_name=constraint_name) + + if self.engine.name == 'mysql': + self.assertTrue(self.does_index_exist('role', constraint_name)) + self.assertFalse(self.does_index_exist('role', 'ixu_role_name')) + else: + self.assertTrue(self.does_constraint_exist('role', + constraint_name)) + self.assertFalse(self.does_constraint_exist('role', + 'ixu_role_name')) + + self.upgrade(88) + if self.engine.name == 'mysql': + self.assertFalse(self.does_index_exist('role', constraint_name)) + self.assertFalse(self.does_index_exist('role', 'ixu_role_name')) + else: + self.assertFalse(self.does_constraint_exist('role', + constraint_name)) + self.assertFalse(self.does_constraint_exist('role', + 'ixu_role_name')) + + def test_migration_96(self): + self.upgrade(95) + if self.engine.name == 'mysql': + self.assertFalse(self.does_index_exist('role', 'ixu_role_name')) + else: + self.assertFalse(self.does_constraint_exist('role', + 'ixu_role_name')) + + self.upgrade(96) + if self.engine.name == 'mysql': + self.assertFalse(self.does_index_exist('role', 'ixu_role_name')) + else: + self.assertFalse(self.does_constraint_exist('role', + 'ixu_role_name')) + + def test_migration_96_constraint_exists(self): + self.upgrade(95) + self._add_unique_constraint_to_role_name() + + if self.engine.name == 'mysql': + self.assertTrue(self.does_index_exist('role', 'ixu_role_name')) + else: + self.assertTrue(self.does_constraint_exist('role', + 'ixu_role_name')) + + self.upgrade(96) + if self.engine.name == 'mysql': + self.assertFalse(self.does_index_exist('role', 'ixu_role_name')) + else: + self.assertFalse(self.does_constraint_exist('role', + 'ixu_role_name')) class VersionTests(SqlMigrateBase): - _initial_db_version = migrate_repo.DB_INIT_VERSION + _initial_db_version = migration_helpers.get_init_version() def test_core_initial(self): """Get the version before migrated, it's the initial DB version.""" version = migration_helpers.get_db_version() - self.assertEqual(migrate_repo.DB_INIT_VERSION, version) + self.assertEqual(self._initial_db_version, version) def test_core_max(self): """When get the version after upgrading, it's the new version.""" @@ -793,97 +1181,15 @@ class VersionTests(SqlMigrateBase): migration_helpers.get_db_version, extension='federation') - def test_extension_initial(self): - """When get the initial version of an extension, it's 0.""" - for name, extension in EXTENSIONS.items(): - abs_path = migration_helpers.find_migrate_repo(extension) - migration.db_version_control(sql.get_engine(), abs_path) - version = migration_helpers.get_db_version(extension=name) - self.assertEqual(0, version, - 'Migrate version for %s is not 0' % name) - - def test_extension_migrated(self): - """When get the version after migrating an extension, it's not 0.""" - for name, extension in EXTENSIONS.items(): - abs_path = migration_helpers.find_migrate_repo(extension) - migration.db_version_control(sql.get_engine(), abs_path) - migration.db_sync(sql.get_engine(), abs_path) - version = migration_helpers.get_db_version(extension=name) - self.assertTrue( - version > 0, - "Version for %s didn't change after migrated?" % name) - # Verify downgrades cannot occur - self.assertRaises( - db_exception.DbMigrationError, - migration_helpers._sync_extension_repo, - extension=name, - version=0) - - def test_extension_federation_upgraded_values(self): - abs_path = migration_helpers.find_migrate_repo(federation) - migration.db_version_control(sql.get_engine(), abs_path) - migration.db_sync(sql.get_engine(), abs_path, version=6) - idp_table = sqlalchemy.Table("identity_provider", - self.metadata, - autoload=True) - idps = [{'id': uuid.uuid4().hex, - 'enabled': True, - 'description': uuid.uuid4().hex, - 'remote_id': uuid.uuid4().hex}, - {'id': uuid.uuid4().hex, - 'enabled': True, - 'description': uuid.uuid4().hex, - 'remote_id': uuid.uuid4().hex}] - for idp in idps: - ins = idp_table.insert().values({'id': idp['id'], - 'enabled': idp['enabled'], - 'description': idp['description'], - 'remote_id': idp['remote_id']}) - self.engine.execute(ins) - migration.db_sync(sql.get_engine(), abs_path) - idp_remote_ids_table = sqlalchemy.Table("idp_remote_ids", - self.metadata, - autoload=True) - for idp in idps: - s = idp_remote_ids_table.select().where( - idp_remote_ids_table.c.idp_id == idp['id']) - remote = self.engine.execute(s).fetchone() - self.assertEqual(idp['remote_id'], - remote['remote_id'], - 'remote_ids must be preserved during the ' - 'migration from identity_provider table to ' - 'idp_remote_ids table') - def test_unexpected_extension(self): - """The version for an extension that doesn't exist raises ImportError. - - """ - + """The version for a non-existent extension raises ImportError.""" extension_name = uuid.uuid4().hex self.assertRaises(ImportError, migration_helpers.get_db_version, extension=extension_name) def test_unversioned_extension(self): - """The version for extensions without migrations raise an exception. - - """ - + """The version for extensions without migrations raise an exception.""" self.assertRaises(exception.MigrationNotProvided, migration_helpers.get_db_version, extension='admin_crud') - - def test_initial_with_extension_version_None(self): - """When performing a default migration, also migrate extensions.""" - migration_helpers.sync_database_to_version(extension=None, - version=None) - for table in INITIAL_EXTENSION_TABLE_STRUCTURE: - self.assertTableColumns(table, - INITIAL_EXTENSION_TABLE_STRUCTURE[table]) - - def test_initial_with_extension_version_max(self): - """When migrating to max version, do not migrate extensions.""" - migration_helpers.sync_database_to_version(extension=None, - version=self.max_version) - for table in INITIAL_EXTENSION_TABLE_STRUCTURE: - self.assertTableDoesNotExist(table) diff --git a/keystone-moon/keystone/tests/unit/test_token_provider.py b/keystone-moon/keystone/tests/unit/test_token_provider.py index f60f7d53..5c71363b 100644 --- a/keystone-moon/keystone/tests/unit/test_token_provider.py +++ b/keystone-moon/keystone/tests/unit/test_token_provider.py @@ -16,6 +16,7 @@ import datetime from oslo_config import cfg from oslo_utils import timeutils +from six.moves import reload_module from keystone.common import dependency from keystone.common import utils @@ -781,6 +782,12 @@ class TestTokenProvider(unit.TestCase): self.assertIsNone( self.token_provider_api._is_valid_token(create_v3_token())) + def test_no_token_raises_token_not_found(self): + self.assertRaises( + exception.TokenNotFound, + self.token_provider_api.validate_token, + None) + # NOTE(ayoung): renamed to avoid automatic test detection class PKIProviderTests(object): @@ -803,7 +810,8 @@ class PKIProviderTests(object): self.cms.subprocess = self.target_subprocess self.environment.subprocess = self.target_subprocess - reload(pki) # force module reload so the imports get re-evaluated + # force module reload so the imports get re-evaluated + reload_module(pki) def test_get_token_id_error_handling(self): # cause command-line failure diff --git a/keystone-moon/keystone/tests/unit/test_url_middleware.py b/keystone-moon/keystone/tests/unit/test_url_middleware.py index 217b302d..3b160b93 100644 --- a/keystone-moon/keystone/tests/unit/test_url_middleware.py +++ b/keystone-moon/keystone/tests/unit/test_url_middleware.py @@ -20,6 +20,7 @@ from keystone.tests import unit class FakeApp(object): """Fakes a WSGI app URL normalized.""" + def __call__(self, env, start_response): resp = webob.Response() resp.body = 'SUCCESS' diff --git a/keystone-moon/keystone/tests/unit/test_v2.py b/keystone-moon/keystone/tests/unit/test_v2.py index acdfca5f..e81c6040 100644 --- a/keystone-moon/keystone/tests/unit/test_v2.py +++ b/keystone-moon/keystone/tests/unit/test_v2.py @@ -23,9 +23,11 @@ from six.moves import http_client from testtools import matchers from keystone.common import extension as keystone_extension +from keystone.tests import unit +from keystone.tests.unit import default_fixtures from keystone.tests.unit import ksfixtures from keystone.tests.unit import rest - +from keystone.tests.unit.schema import v2 CONF = cfg.CONF @@ -106,11 +108,11 @@ class CoreApiTests(object): self.assertValidExtensionListResponse( r, keystone_extension.ADMIN_EXTENSIONS) - def test_admin_extensions_404(self): + def test_admin_extensions_returns_not_found(self): self.admin_request(path='/v2.0/extensions/invalid-extension', expected_status=http_client.NOT_FOUND) - def test_public_osksadm_extension_404(self): + def test_public_osksadm_extension_returns_not_found(self): self.public_request(path='/v2.0/extensions/OS-KSADM', expected_status=http_client.NOT_FOUND) @@ -132,7 +134,7 @@ class CoreApiTests(object): 'tenantId': self.tenant_bar['id'], }, }, - expected_status=200) + expected_status=http_client.OK) self.assertValidAuthenticationResponse(r, require_service_catalog=True) def test_authenticate_unscoped(self): @@ -147,7 +149,7 @@ class CoreApiTests(object): }, }, }, - expected_status=200) + expected_status=http_client.OK) self.assertValidAuthenticationResponse(r) def test_get_tenants_for_token(self): @@ -164,7 +166,7 @@ class CoreApiTests(object): token=token) self.assertValidAuthenticationResponse(r) - def test_invalid_token_404(self): + def test_invalid_token_returns_not_found(self): token = self.get_scoped_token() self.admin_request( path='/v2.0/tokens/%(token_id)s' % { @@ -179,7 +181,8 @@ class CoreApiTests(object): self.tenant_service['id'], self.role_service['id']) - token = self.get_scoped_token(tenant_id='service') + token = self.get_scoped_token( + tenant_id=default_fixtures.SERVICE_TENANT_ID) r = self.admin_request( path='/v2.0/tokens/%s' % token, token=token) @@ -191,7 +194,8 @@ class CoreApiTests(object): self.tenant_service['id'], self.role_service['id']) - token = self.get_scoped_token(tenant_id='service') + token = self.get_scoped_token( + tenant_id=default_fixtures.SERVICE_TENANT_ID) r = self.admin_request( path='/v2.0/tokens/%s' % token, token=token) @@ -234,7 +238,7 @@ class CoreApiTests(object): 'token_id': token, }, token=token, - expected_status=200) + expected_status=http_client.OK) def test_endpoints(self): token = self.get_scoped_token() @@ -273,6 +277,14 @@ class CoreApiTests(object): token=token) self.assertValidRoleListResponse(r) + def test_get_user_roles_without_tenant(self): + token = self.get_scoped_token() + self.admin_request( + path='/v2.0/users/%(user_id)s/roles' % { + 'user_id': self.user_foo['id'], + }, + token=token, expected_status=http_client.NOT_IMPLEMENTED) + def test_get_user(self): token = self.get_scoped_token() r = self.admin_request( @@ -370,7 +382,7 @@ class CoreApiTests(object): }, }, token=token, - expected_status=200) + expected_status=http_client.OK) def test_error_response(self): """This triggers assertValidErrorResponse by convention.""" @@ -459,7 +471,7 @@ class CoreApiTests(object): }, }, token=token, - expected_status=200) + expected_status=http_client.OK) user_id = self._get_user_id(r.result) @@ -470,7 +482,7 @@ class CoreApiTests(object): 'user_id': user_id }, token=token, - expected_status=200) + expected_status=http_client.OK) self.assertEqual(CONF.member_role_name, self._get_role_name(r.result)) # Create a new tenant @@ -485,7 +497,7 @@ class CoreApiTests(object): }, }, token=token, - expected_status=200) + expected_status=http_client.OK) project_id = self._get_project_id(r.result) @@ -501,7 +513,7 @@ class CoreApiTests(object): }, }, token=token, - expected_status=200) + expected_status=http_client.OK) # 'member_role' should be in new_tenant r = self.admin_request( @@ -510,7 +522,7 @@ class CoreApiTests(object): 'user_id': user_id }, token=token, - expected_status=200) + expected_status=http_client.OK) self.assertEqual('_member_', self._get_role_name(r.result)) # 'member_role' should not be in tenant_bar any more @@ -520,7 +532,7 @@ class CoreApiTests(object): 'user_id': user_id }, token=token, - expected_status=200) + expected_status=http_client.OK) self.assertNoRoles(r.result) def test_update_user_with_invalid_tenant(self): @@ -539,7 +551,7 @@ class CoreApiTests(object): }, }, token=token, - expected_status=200) + expected_status=http_client.OK) user_id = self._get_user_id(r.result) # Update user with an invalid tenant @@ -571,7 +583,7 @@ class CoreApiTests(object): }, }, token=token, - expected_status=200) + expected_status=http_client.OK) user_id = self._get_user_id(r.result) # Update user with an invalid tenant @@ -604,7 +616,7 @@ class CoreApiTests(object): }, }, token=token, - expected_status=200) + expected_status=http_client.OK) user_id = self._get_user_id(r.result) @@ -615,7 +627,7 @@ class CoreApiTests(object): 'user_id': user_id }, token=token, - expected_status=200) + expected_status=http_client.OK) self.assertEqual(CONF.member_role_name, self._get_role_name(r.result)) # Update user's tenant with old tenant id @@ -630,7 +642,7 @@ class CoreApiTests(object): }, }, token=token, - expected_status=200) + expected_status=http_client.OK) # 'member_role' should still be in tenant_bar r = self.admin_request( @@ -639,7 +651,7 @@ class CoreApiTests(object): 'user_id': user_id }, token=token, - expected_status=200) + expected_status=http_client.OK) self.assertEqual('_member_', self._get_role_name(r.result)) def test_authenticating_a_user_with_no_password(self): @@ -721,7 +733,7 @@ class LegacyV2UsernameTests(object): path='/v2.0/users', token=token, body=body, - expected_status=200) + expected_status=http_client.OK) def test_create_with_extra_username(self): """The response for creating a user will contain the extra fields.""" @@ -772,7 +784,7 @@ class LegacyV2UsernameTests(object): 'enabled': enabled, }, }, - expected_status=200) + expected_status=http_client.OK) self.assertValidUserResponse(r) @@ -802,7 +814,7 @@ class LegacyV2UsernameTests(object): 'enabled': enabled, }, }, - expected_status=200) + expected_status=http_client.OK) self.assertValidUserResponse(r) @@ -881,7 +893,7 @@ class LegacyV2UsernameTests(object): 'enabled': enabled, }, }, - expected_status=200) + expected_status=http_client.OK) self.assertValidUserResponse(r) @@ -911,7 +923,7 @@ class LegacyV2UsernameTests(object): 'enabled': enabled, }, }, - expected_status=200) + expected_status=http_client.OK) self.assertValidUserResponse(r) @@ -931,7 +943,7 @@ class LegacyV2UsernameTests(object): 'enabled': True, }, }, - expected_status=200) + expected_status=http_client.OK) self.assertValidUserResponse(r) @@ -956,7 +968,7 @@ class LegacyV2UsernameTests(object): 'enabled': enabled, }, }, - expected_status=200) + expected_status=http_client.OK) self.assertValidUserResponse(r) @@ -979,6 +991,14 @@ class RestfulTestCase(rest.RestfulTestCase): class V2TestCase(RestfulTestCase, CoreApiTests, LegacyV2UsernameTests): + + def config_overrides(self): + super(V2TestCase, self).config_overrides() + self.config_fixture.config( + group='catalog', + driver='templated', + template_file=unit.dirs.tests('default_catalog.templates')) + def _get_user_id(self, r): return r['user']['id'] @@ -1200,7 +1220,7 @@ class V2TestCase(RestfulTestCase, CoreApiTests, LegacyV2UsernameTests): method='GET', path='/v2.0/tokens/revoked', token=token, - expected_status=200) + expected_status=http_client.OK) self.assertValidRevocationListResponse(r) def assertValidRevocationListResponse(self, response): @@ -1231,7 +1251,7 @@ class V2TestCase(RestfulTestCase, CoreApiTests, LegacyV2UsernameTests): method='GET', path='/v2.0/tokens/revoked', token=token1, - expected_status=200) + expected_status=http_client.OK) signed_text = r.result['signed'] data_json = cms.cms_verify(signed_text, CONF.signing.certfile, @@ -1242,10 +1262,11 @@ class V2TestCase(RestfulTestCase, CoreApiTests, LegacyV2UsernameTests): return (data, token2) def test_fetch_revocation_list_md5(self): - """If the server is configured for md5, then the revocation list has - tokens hashed with MD5. - """ + """Hash for tokens in revocation list and server config should match. + If the server is configured for md5, then the revocation list has + tokens hashed with MD5. + """ # The default hash algorithm is md5. hash_algorithm = 'md5' @@ -1254,10 +1275,11 @@ class V2TestCase(RestfulTestCase, CoreApiTests, LegacyV2UsernameTests): self.assertThat(token_hash, matchers.Equals(data['revoked'][0]['id'])) def test_fetch_revocation_list_sha256(self): - """If the server is configured for sha256, then the revocation list has - tokens hashed with SHA256 - """ + """Hash for tokens in revocation list and server config should match. + If the server is configured for sha256, then the revocation list has + tokens hashed with SHA256. + """ hash_algorithm = 'sha256' self.config_fixture.config(group='token', hash_algorithm=hash_algorithm) @@ -1333,7 +1355,7 @@ class V2TestCase(RestfulTestCase, CoreApiTests, LegacyV2UsernameTests): }, }, }, - expected_status=200) + expected_status=http_client.OK) # ensure password doesn't leak user_id = r.result['user']['id'] @@ -1341,7 +1363,7 @@ class V2TestCase(RestfulTestCase, CoreApiTests, LegacyV2UsernameTests): method='GET', path='/v2.0/users/%s' % user_id, token=token, - expected_status=200) + expected_status=http_client.OK) self.assertNotIn('OS-KSADM:password', r.result['user']) def test_updating_a_user_with_an_OSKSADM_password(self): @@ -1360,7 +1382,7 @@ class V2TestCase(RestfulTestCase, CoreApiTests, LegacyV2UsernameTests): }, }, token=token, - expected_status=200) + expected_status=http_client.OK) # successfully authenticate self.public_request( @@ -1374,13 +1396,12 @@ class V2TestCase(RestfulTestCase, CoreApiTests, LegacyV2UsernameTests): }, }, }, - expected_status=200) + expected_status=http_client.OK) class RevokeApiTestCase(V2TestCase): def config_overrides(self): super(RevokeApiTestCase, self).config_overrides() - self.config_fixture.config(group='revoke', driver='kvs') self.config_fixture.config( group='token', provider='pki', @@ -1402,6 +1423,27 @@ class TestFernetTokenProviderV2(RestfulTestCase): super(TestFernetTokenProviderV2, self).setUp() self.useFixture(ksfixtures.KeyRepository(self.config_fixture)) + # Add catalog data + self.region = unit.new_region_ref() + self.region_id = self.region['id'] + self.catalog_api.create_region(self.region) + + self.service = unit.new_service_ref() + self.service_id = self.service['id'] + self.catalog_api.create_service(self.service_id, self.service) + + self.endpoint = unit.new_endpoint_ref(service_id=self.service_id, + interface='public', + region_id=self.region_id) + self.endpoint_id = self.endpoint['id'] + self.catalog_api.create_endpoint(self.endpoint_id, self.endpoint) + + def assertValidUnscopedTokenResponse(self, r): + v2.unscoped_validator.validate(r.json['access']) + + def assertValidScopedTokenResponse(self, r): + v2.scoped_validator.validate(r.json['access']) + # Used by RestfulTestCase def _get_token_id(self, r): return r.result['access']['token']['id'] @@ -1432,11 +1474,12 @@ class TestFernetTokenProviderV2(RestfulTestCase): admin_token = self.get_scoped_token(tenant_id=project_ref['id']) unscoped_token = self.get_unscoped_token() path = ('/v2.0/tokens/%s' % unscoped_token) - self.admin_request( + resp = self.admin_request( method='GET', path=path, token=admin_token, - expected_status=200) + expected_status=http_client.OK) + self.assertValidUnscopedTokenResponse(resp) def test_authenticate_scoped_token(self): project_ref = self.new_project_ref() @@ -1462,11 +1505,12 @@ class TestFernetTokenProviderV2(RestfulTestCase): path = ('/v2.0/tokens/%s?belongsTo=%s' % (member_token, project2_ref['id'])) # Validate token belongs to project - self.admin_request( + resp = self.admin_request( method='GET', path=path, token=admin_token, - expected_status=200) + expected_status=http_client.OK) + self.assertValidScopedTokenResponse(resp) def test_token_authentication_and_validation(self): """Test token authentication for Fernet token provider. @@ -1491,16 +1535,17 @@ class TestFernetTokenProviderV2(RestfulTestCase): } } }, - expected_status=200) + expected_status=http_client.OK) token_id = self._get_token_id(r) path = ('/v2.0/tokens/%s?belongsTo=%s' % (token_id, project_ref['id'])) # Validate token belongs to project - self.admin_request( + resp = self.admin_request( method='GET', path=path, - token=CONF.admin_token, - expected_status=200) + token=self.get_admin_token(), + expected_status=http_client.OK) + self.assertValidScopedTokenResponse(resp) def test_rescoped_tokens_maintain_original_expiration(self): project_ref = self.new_project_ref() @@ -1522,7 +1567,7 @@ class TestFernetTokenProviderV2(RestfulTestCase): }, # NOTE(lbragstad): This test may need to be refactored if Keystone # decides to disallow rescoping using a scoped token. - expected_status=200) + expected_status=http_client.OK) original_token = resp.result['access']['token']['id'] original_expiration = resp.result['access']['token']['expires'] @@ -1537,8 +1582,9 @@ class TestFernetTokenProviderV2(RestfulTestCase): } } }, - expected_status=200) + expected_status=http_client.OK) rescoped_token = resp.result['access']['token']['id'] rescoped_expiration = resp.result['access']['token']['expires'] self.assertNotEqual(original_token, rescoped_token) self.assertEqual(original_expiration, rescoped_expiration) + self.assertValidScopedTokenResponse(resp) diff --git a/keystone-moon/keystone/tests/unit/test_v2_controller.py b/keystone-moon/keystone/tests/unit/test_v2_controller.py index 581e6b9c..6cf8bc53 100644 --- a/keystone-moon/keystone/tests/unit/test_v2_controller.py +++ b/keystone-moon/keystone/tests/unit/test_v2_controller.py @@ -13,8 +13,11 @@ # under the License. +import copy import uuid +from testtools import matchers + from keystone.assignment import controllers as assignment_controllers from keystone import exception from keystone.resource import controllers as resource_controllers @@ -32,6 +35,7 @@ class TenantTestCase(unit.TestCase): These tests exercise :class:`keystone.assignment.controllers.Tenant`. """ + def setUp(self): super(TenantTestCase, self).setUp() self.useFixture(database.Database()) @@ -73,17 +77,18 @@ class TenantTestCase(unit.TestCase): def test_list_projects_default_domain(self): """Test that list projects only returns those in the default domain.""" - - domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, - 'enabled': True} + domain = unit.new_domain_ref() self.resource_api.create_domain(domain['id'], domain) - project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, - 'domain_id': domain['id']} + project1 = unit.new_project_ref(domain_id=domain['id']) self.resource_api.create_project(project1['id'], project1) - # Check the real total number of projects, we should have the above - # plus those in the default features + # Check the real total number of projects, we should have the: + # - tenants in the default fixtures + # - the project representing the default domain + # - the project representing the domain we created above + # - the project we created above refs = self.resource_api.list_projects() - self.assertEqual(len(default_fixtures.TENANTS) + 1, len(refs)) + self.assertThat( + refs, matchers.HasLength(len(default_fixtures.TENANTS) + 3)) # Now list all projects using the v2 API - we should only get # back those in the default features, since only those are in the @@ -98,11 +103,52 @@ class TenantTestCase(unit.TestCase): self.assertIn(tenant_copy, refs['tenants']) def _create_is_domain_project(self): - project = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, - 'domain_id': 'default', 'is_domain': True} + project = unit.new_project_ref(is_domain=True) project_ref = self.resource_api.create_project(project['id'], project) return self.tenant_controller.v3_to_v2_project(project_ref) + def test_get_is_domain_project_not_found(self): + """Test that get project does not return is_domain projects.""" + project = self._create_is_domain_project() + + context = copy.deepcopy(_ADMIN_CONTEXT) + context['query_string']['name'] = project['name'] + + self.assertRaises( + exception.ProjectNotFound, + self.tenant_controller.get_all_projects, + context) + + context = copy.deepcopy(_ADMIN_CONTEXT) + context['query_string']['name'] = project['id'] + + self.assertRaises( + exception.ProjectNotFound, + self.tenant_controller.get_all_projects, + context) + + def test_create_is_domain_project_fails(self): + """Test that the creation of a project acting as a domain fails.""" + project = {'name': uuid.uuid4().hex, 'domain_id': 'default', + 'is_domain': True} + + self.assertRaises( + exception.ValidationError, + self.tenant_controller.create_project, + _ADMIN_CONTEXT, + project) + + def test_create_project_passing_is_domain_false_fails(self): + """Test that passing is_domain=False is not allowed.""" + project = {'name': uuid.uuid4().hex, 'domain_id': 'default', + 'is_domain': False} + + self.assertRaises( + exception.ValidationError, + self.tenant_controller.create_project, + _ADMIN_CONTEXT, + project) + def test_update_is_domain_project_not_found(self): """Test that update is_domain project is not allowed in v2.""" project = self._create_is_domain_project() @@ -113,8 +159,7 @@ class TenantTestCase(unit.TestCase): self.tenant_controller.update_project, _ADMIN_CONTEXT, project['id'], - project - ) + project) def test_delete_is_domain_project_not_found(self): """Test that delete is_domain project is not allowed in v2.""" @@ -124,14 +169,12 @@ class TenantTestCase(unit.TestCase): exception.ProjectNotFound, self.tenant_controller.delete_project, _ADMIN_CONTEXT, - project['id'] - ) + project['id']) def test_list_is_domain_project_not_found(self): """Test v2 get_all_projects having projects that act as a domain. - In v2 no project with the is_domain flag enabled should be - returned. + In v2 no project with the is_domain flag enabled should be returned. """ project1 = self._create_is_domain_project() project2 = self._create_is_domain_project() diff --git a/keystone-moon/keystone/tests/unit/test_v3.py b/keystone-moon/keystone/tests/unit/test_v3.py index 32c5e295..216d8c79 100644 --- a/keystone-moon/keystone/tests/unit/test_v3.py +++ b/keystone-moon/keystone/tests/unit/test_v3.py @@ -12,20 +12,25 @@ # License for the specific language governing permissions and limitations # under the License. -import datetime import uuid +import mock from oslo_config import cfg +import oslo_context.context from oslo_serialization import jsonutils from oslo_utils import timeutils +from six.moves import http_client from testtools import matchers +import webtest from keystone import auth from keystone.common import authorization from keystone.common import cache +from keystone.common.validation import validators from keystone import exception from keystone import middleware -from keystone.policy.backends import rules +from keystone.middleware import auth as middleware_auth +from keystone.tests.common import auth as common_auth from keystone.tests import unit from keystone.tests.unit import rest @@ -38,6 +43,7 @@ TIME_FORMAT = unit.TIME_FORMAT class AuthTestMixin(object): """To hold auth building helper functions.""" + def build_auth_scope(self, project_id=None, project_name=None, project_domain_id=None, project_domain_name=None, domain_id=None, domain_name=None, trust_id=None, @@ -116,7 +122,127 @@ class AuthTestMixin(object): class RestfulTestCase(unit.SQLDriverOverrides, rest.RestfulTestCase, - AuthTestMixin): + common_auth.AuthTestMixin): + + def generate_token_schema(self, domain_scoped=False, project_scoped=False): + """Return a dictionary of token properties to validate against.""" + properties = { + 'audit_ids': { + 'type': 'array', + 'items': { + 'type': 'string', + }, + 'minItems': 1, + 'maxItems': 2, + }, + 'bind': { + 'type': 'object', + 'properties': { + 'kerberos': { + 'type': 'string', + }, + }, + 'required': ['kerberos'], + 'additionalProperties': False, + }, + 'expires_at': {'type': 'string'}, + 'issued_at': {'type': 'string'}, + 'methods': { + 'type': 'array', + 'items': { + 'type': 'string', + }, + }, + 'user': { + 'type': 'object', + 'required': ['id', 'name', 'domain'], + 'properties': { + 'id': {'type': 'string'}, + 'name': {'type': 'string'}, + 'domain': { + 'type': 'object', + 'properties': { + 'id': {'type': 'string'}, + 'name': {'type': 'string'} + }, + 'required': ['id', 'name'], + 'additonalProperties': False, + } + }, + 'additionalProperties': False, + } + } + + if domain_scoped: + properties['catalog'] = {'type': 'array'} + properties['roles'] = { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'id': {'type': 'string', }, + 'name': {'type': 'string', }, + }, + 'required': ['id', 'name', ], + 'additionalProperties': False, + }, + 'minItems': 1, + } + properties['domain'] = { + 'domain': { + 'type': 'object', + 'required': ['id', 'name'], + 'properties': { + 'id': {'type': 'string'}, + 'name': {'type': 'string'} + }, + 'additionalProperties': False + } + } + elif project_scoped: + properties['is_admin_project'] = {'type': 'boolean'} + properties['catalog'] = {'type': 'array'} + properties['roles'] = {'type': 'array'} + properties['project'] = { + 'type': ['object'], + 'required': ['id', 'name', 'domain'], + 'properties': { + 'id': {'type': 'string'}, + 'name': {'type': 'string'}, + 'domain': { + 'type': ['object'], + 'required': ['id', 'name'], + 'properties': { + 'id': {'type': 'string'}, + 'name': {'type': 'string'} + }, + 'additionalProperties': False + } + }, + 'additionalProperties': False + } + + schema = { + 'type': 'object', + 'properties': properties, + 'required': ['audit_ids', 'expires_at', 'issued_at', 'methods', + 'user'], + 'optional': ['bind'], + 'additionalProperties': False + } + + if domain_scoped: + schema['required'].extend(['domain', 'roles']) + schema['optional'].append('catalog') + elif project_scoped: + schema['required'].append('project') + schema['optional'].append('bind') + schema['optional'].append('catalog') + schema['optional'].append('OS-TRUST:trust') + schema['optional'].append('is_admin_project') + + return schema + def config_files(self): config_files = super(RestfulTestCase, self).config_files() config_files.append(unit.dirs.tests_conf('backend_sql.conf')) @@ -146,9 +272,7 @@ class RestfulTestCase(unit.SQLDriverOverrides, rest.RestfulTestCase, pass def setUp(self, app_conf='keystone'): - """Setup for v3 Restful Test Cases. - - """ + """Setup for v3 Restful Test Cases.""" new_paste_file = self.generate_paste_config() self.addCleanup(self.remove_generated_paste_config) if new_paste_file: @@ -158,16 +282,9 @@ class RestfulTestCase(unit.SQLDriverOverrides, rest.RestfulTestCase, self.empty_context = {'environment': {}} - # Initialize the policy engine and allow us to write to a temp - # file in each test to create the policies - rules.reset() - - # drop the policy rules - self.addCleanup(rules.reset) - def load_backends(self): # ensure the cache region instance is setup - cache.configure_cache_region(cache.REGION) + cache.configure_cache() super(RestfulTestCase, self).load_backends() @@ -183,53 +300,42 @@ class RestfulTestCase(unit.SQLDriverOverrides, rest.RestfulTestCase, try: self.resource_api.get_domain(DEFAULT_DOMAIN_ID) except exception.DomainNotFound: - domain = {'description': (u'Owns users and tenants (i.e. ' - u'projects) available on Identity ' - u'API v2.'), - 'enabled': True, - 'id': DEFAULT_DOMAIN_ID, - 'name': u'Default'} + domain = unit.new_domain_ref( + description=(u'The default domain'), + id=DEFAULT_DOMAIN_ID, + name=u'Default') self.resource_api.create_domain(DEFAULT_DOMAIN_ID, domain) def load_sample_data(self): self._populate_default_domain() - self.domain_id = uuid.uuid4().hex - self.domain = self.new_domain_ref() - self.domain['id'] = self.domain_id + self.domain = unit.new_domain_ref() + self.domain_id = self.domain['id'] self.resource_api.create_domain(self.domain_id, self.domain) - self.project_id = uuid.uuid4().hex - self.project = self.new_project_ref( - domain_id=self.domain_id) - self.project['id'] = self.project_id - self.resource_api.create_project(self.project_id, self.project) + self.project = unit.new_project_ref(domain_id=self.domain_id) + self.project_id = self.project['id'] + self.project = self.resource_api.create_project(self.project_id, + self.project) - self.user = self.new_user_ref(domain_id=self.domain_id) - password = self.user['password'] - self.user = self.identity_api.create_user(self.user) - self.user['password'] = password + self.user = unit.create_user(self.identity_api, + domain_id=self.domain_id) self.user_id = self.user['id'] self.default_domain_project_id = uuid.uuid4().hex - self.default_domain_project = self.new_project_ref( + self.default_domain_project = unit.new_project_ref( domain_id=DEFAULT_DOMAIN_ID) self.default_domain_project['id'] = self.default_domain_project_id self.resource_api.create_project(self.default_domain_project_id, self.default_domain_project) - self.default_domain_user = self.new_user_ref( + self.default_domain_user = unit.create_user( + self.identity_api, domain_id=DEFAULT_DOMAIN_ID) - password = self.default_domain_user['password'] - self.default_domain_user = ( - self.identity_api.create_user(self.default_domain_user)) - self.default_domain_user['password'] = password self.default_domain_user_id = self.default_domain_user['id'] # create & grant policy.json's default role for admin_required - self.role_id = uuid.uuid4().hex - self.role = self.new_role_ref() - self.role['id'] = self.role_id - self.role['name'] = 'admin' + self.role = unit.new_role_ref(name='admin') + self.role_id = self.role['id'] self.role_api.create_role(self.role_id, self.role) self.assignment_api.add_role_to_user_and_project( self.user_id, self.project_id, self.role_id) @@ -240,81 +346,35 @@ class RestfulTestCase(unit.SQLDriverOverrides, rest.RestfulTestCase, self.default_domain_user_id, self.project_id, self.role_id) - self.region_id = uuid.uuid4().hex - self.region = self.new_region_ref() - self.region['id'] = self.region_id - self.catalog_api.create_region( - self.region.copy()) - - self.service_id = uuid.uuid4().hex - self.service = self.new_service_ref() - self.service['id'] = self.service_id - self.catalog_api.create_service( - self.service_id, - self.service.copy()) - - self.endpoint_id = uuid.uuid4().hex - self.endpoint = self.new_endpoint_ref(service_id=self.service_id) - self.endpoint['id'] = self.endpoint_id - self.endpoint['region_id'] = self.region['id'] - self.catalog_api.create_endpoint( - self.endpoint_id, - self.endpoint.copy()) - # The server adds 'enabled' and defaults to True. - self.endpoint['enabled'] = True - - def new_ref(self): - """Populates a ref with attributes common to some API entities.""" - return unit.new_ref() - - def new_region_ref(self): - return unit.new_region_ref() - - def new_service_ref(self): - return unit.new_service_ref() - - def new_endpoint_ref(self, service_id, interface='public', **kwargs): - return unit.new_endpoint_ref( - service_id, interface=interface, default_region_id=self.region_id, - **kwargs) - - def new_domain_ref(self): - return unit.new_domain_ref() - - def new_project_ref(self, domain_id=None, parent_id=None, is_domain=False): - return unit.new_project_ref(domain_id=domain_id, parent_id=parent_id, - is_domain=is_domain) - - def new_user_ref(self, domain_id, project_id=None): - return unit.new_user_ref(domain_id, project_id=project_id) - - def new_group_ref(self, domain_id): - return unit.new_group_ref(domain_id) - - def new_credential_ref(self, user_id, project_id=None, cred_type=None): - return unit.new_credential_ref(user_id, project_id=project_id, - cred_type=cred_type) + # Create "req_admin" user for simulating a real user instead of the + # admin_token_auth middleware + self.user_reqadmin = unit.create_user(self.identity_api, + DEFAULT_DOMAIN_ID) + self.assignment_api.add_role_to_user_and_project( + self.user_reqadmin['id'], + self.default_domain_project_id, + self.role_id) - def new_role_ref(self): - return unit.new_role_ref() + self.region = unit.new_region_ref() + self.region_id = self.region['id'] + self.catalog_api.create_region(self.region) - def new_policy_ref(self): - return unit.new_policy_ref() + self.service = unit.new_service_ref() + self.service_id = self.service['id'] + self.catalog_api.create_service(self.service_id, self.service.copy()) - def new_trust_ref(self, trustor_user_id, trustee_user_id, project_id=None, - impersonation=None, expires=None, role_ids=None, - role_names=None, remaining_uses=None, - allow_redelegation=False): - return unit.new_trust_ref( - trustor_user_id, trustee_user_id, project_id=project_id, - impersonation=impersonation, expires=expires, role_ids=role_ids, - role_names=role_names, remaining_uses=remaining_uses, - allow_redelegation=allow_redelegation) + self.endpoint = unit.new_endpoint_ref(service_id=self.service_id, + interface='public', + region_id=self.region_id) + self.endpoint_id = self.endpoint['id'] + self.catalog_api.create_endpoint(self.endpoint_id, + self.endpoint.copy()) + # The server adds 'enabled' and defaults to True. + self.endpoint['enabled'] = True def create_new_default_project_for_user(self, user_id, domain_id, enable_project=True): - ref = self.new_project_ref(domain_id=domain_id) - ref['enabled'] = enable_project + ref = unit.new_project_ref(domain_id=domain_id, enabled=enable_project) r = self.post('/projects', body={'project': ref}) project = self.assertValidProjectResponse(r, ref) # set the user's preferred project @@ -326,6 +386,34 @@ class RestfulTestCase(unit.SQLDriverOverrides, rest.RestfulTestCase, return project + def get_admin_token(self): + """Convenience method so that we can test authenticated requests.""" + r = self.admin_request( + method='POST', + path='/v3/auth/tokens', + body={ + 'auth': { + 'identity': { + 'methods': ['password'], + 'password': { + 'user': { + 'name': self.user_reqadmin['name'], + 'password': self.user_reqadmin['password'], + 'domain': { + 'id': self.user_reqadmin['domain_id'] + } + } + } + }, + 'scope': { + 'project': { + 'id': self.default_domain_project_id, + } + } + } + }) + return r.headers.get('X-Subject-Token') + def get_unscoped_token(self): """Convenience method so that we can test authenticated requests.""" r = self.admin_request( @@ -407,11 +495,10 @@ class RestfulTestCase(unit.SQLDriverOverrides, rest.RestfulTestCase, def get_requested_token(self, auth): """Request the specific token we want.""" - - r = self.v3_authenticate_token(auth) + r = self.v3_create_token(auth) return r.headers.get('X-Subject-Token') - def v3_authenticate_token(self, auth, expected_status=201): + def v3_create_token(self, auth, expected_status=http_client.CREATED): return self.admin_request(method='POST', path='/v3/auth/tokens', body=auth, @@ -440,42 +527,31 @@ class RestfulTestCase(unit.SQLDriverOverrides, rest.RestfulTestCase, return self.admin_request(path=path, token=token, **kwargs) - def get(self, path, **kwargs): - r = self.v3_request(method='GET', path=path, **kwargs) - if 'expected_status' not in kwargs: - self.assertResponseStatus(r, 200) - return r + def get(self, path, expected_status=http_client.OK, **kwargs): + return self.v3_request(path, method='GET', + expected_status=expected_status, **kwargs) - def head(self, path, **kwargs): - r = self.v3_request(method='HEAD', path=path, **kwargs) - if 'expected_status' not in kwargs: - self.assertResponseStatus(r, 204) - self.assertEqual('', r.body) + def head(self, path, expected_status=http_client.NO_CONTENT, **kwargs): + r = self.v3_request(path, method='HEAD', + expected_status=expected_status, **kwargs) + self.assertEqual(b'', r.body) return r - def post(self, path, **kwargs): - r = self.v3_request(method='POST', path=path, **kwargs) - if 'expected_status' not in kwargs: - self.assertResponseStatus(r, 201) - return r + def post(self, path, expected_status=http_client.CREATED, **kwargs): + return self.v3_request(path, method='POST', + expected_status=expected_status, **kwargs) - def put(self, path, **kwargs): - r = self.v3_request(method='PUT', path=path, **kwargs) - if 'expected_status' not in kwargs: - self.assertResponseStatus(r, 204) - return r + def put(self, path, expected_status=http_client.NO_CONTENT, **kwargs): + return self.v3_request(path, method='PUT', + expected_status=expected_status, **kwargs) - def patch(self, path, **kwargs): - r = self.v3_request(method='PATCH', path=path, **kwargs) - if 'expected_status' not in kwargs: - self.assertResponseStatus(r, 200) - return r + def patch(self, path, expected_status=http_client.OK, **kwargs): + return self.v3_request(path, method='PATCH', + expected_status=expected_status, **kwargs) - def delete(self, path, **kwargs): - r = self.v3_request(method='DELETE', path=path, **kwargs) - if 'expected_status' not in kwargs: - self.assertResponseStatus(r, 204) - return r + def delete(self, path, expected_status=http_client.NO_CONTENT, **kwargs): + return self.v3_request(path, method='DELETE', + expected_status=expected_status, **kwargs) def assertValidErrorResponse(self, r): resp = r.result @@ -582,7 +658,6 @@ class RestfulTestCase(unit.SQLDriverOverrides, rest.RestfulTestCase, except Exception: msg = '%s is not a valid ISO 8601 extended format date time.' % dt raise AssertionError(msg) - self.assertIsInstance(dt, datetime.datetime) def assertValidTokenResponse(self, r, user=None): self.assertTrue(r.headers.get('X-Subject-Token')) @@ -611,11 +686,10 @@ class RestfulTestCase(unit.SQLDriverOverrides, rest.RestfulTestCase, def assertValidUnscopedTokenResponse(self, r, *args, **kwargs): token = self.assertValidTokenResponse(r, *args, **kwargs) - - self.assertNotIn('roles', token) - self.assertNotIn('catalog', token) - self.assertNotIn('project', token) - self.assertNotIn('domain', token) + validator_object = validators.SchemaValidator( + self.generate_token_schema() + ) + validator_object.validate(token) return token @@ -623,6 +697,7 @@ class RestfulTestCase(unit.SQLDriverOverrides, rest.RestfulTestCase, require_catalog = kwargs.pop('require_catalog', True) endpoint_filter = kwargs.pop('endpoint_filter', False) ep_filter_assoc = kwargs.pop('ep_filter_assoc', 0) + is_admin_project = kwargs.pop('is_admin_project', False) token = self.assertValidTokenResponse(r, *args, **kwargs) if require_catalog: @@ -650,40 +725,66 @@ class RestfulTestCase(unit.SQLDriverOverrides, rest.RestfulTestCase, self.assertIn('id', role) self.assertIn('name', role) + if is_admin_project: + # NOTE(samueldmq): We want to explicitly test for boolean + self.assertIs(True, token['is_admin_project']) + else: + self.assertNotIn('is_admin_project', token) + return token def assertValidProjectScopedTokenResponse(self, r, *args, **kwargs): token = self.assertValidScopedTokenResponse(r, *args, **kwargs) - self.assertIn('project', token) - self.assertIn('id', token['project']) - self.assertIn('name', token['project']) - self.assertIn('domain', token['project']) - self.assertIn('id', token['project']['domain']) - self.assertIn('name', token['project']['domain']) + project_scoped_token_schema = self.generate_token_schema( + project_scoped=True) + + if token.get('OS-TRUST:trust'): + trust_properties = { + 'OS-TRUST:trust': { + 'type': ['object'], + 'required': ['id', 'impersonation', 'trustor_user', + 'trustee_user'], + 'properties': { + 'id': {'type': 'string'}, + 'impersonation': {'type': 'boolean'}, + 'trustor_user': { + 'type': 'object', + 'required': ['id'], + 'properties': { + 'id': {'type': 'string'} + }, + 'additionalProperties': False + }, + 'trustee_user': { + 'type': 'object', + 'required': ['id'], + 'properties': { + 'id': {'type': 'string'} + }, + 'additionalProperties': False + } + }, + 'additionalProperties': False + } + } + project_scoped_token_schema['properties'].update(trust_properties) + + validator_object = validators.SchemaValidator( + project_scoped_token_schema) + validator_object.validate(token) self.assertEqual(self.role_id, token['roles'][0]['id']) return token - def assertValidProjectTrustScopedTokenResponse(self, r, *args, **kwargs): - token = self.assertValidProjectScopedTokenResponse(r, *args, **kwargs) - - trust = token.get('OS-TRUST:trust') - self.assertIsNotNone(trust) - self.assertIsNotNone(trust.get('id')) - self.assertIsInstance(trust.get('impersonation'), bool) - self.assertIsNotNone(trust.get('trustor_user')) - self.assertIsNotNone(trust.get('trustee_user')) - self.assertIsNotNone(trust['trustor_user'].get('id')) - self.assertIsNotNone(trust['trustee_user'].get('id')) - def assertValidDomainScopedTokenResponse(self, r, *args, **kwargs): token = self.assertValidScopedTokenResponse(r, *args, **kwargs) - self.assertIn('domain', token) - self.assertIn('id', token['domain']) - self.assertIn('name', token['domain']) + validator_object = validators.SchemaValidator( + self.generate_token_schema(domain_scoped=True) + ) + validator_object.validate(token) return token @@ -876,7 +977,6 @@ class RestfulTestCase(unit.SQLDriverOverrides, rest.RestfulTestCase, **kwargs) def assertValidProject(self, entity, ref=None): - self.assertIsNotNone(entity.get('domain_id')) if ref: self.assertEqual(ref['domain_id'], entity['domain_id']) return entity @@ -888,6 +988,7 @@ class RestfulTestCase(unit.SQLDriverOverrides, rest.RestfulTestCase, resp, 'users', self.assertValidUser, + keys_to_check=['name', 'enabled'], *args, **kwargs) @@ -896,6 +997,7 @@ class RestfulTestCase(unit.SQLDriverOverrides, rest.RestfulTestCase, resp, 'user', self.assertValidUser, + keys_to_check=['name', 'enabled'], *args, **kwargs) @@ -920,6 +1022,7 @@ class RestfulTestCase(unit.SQLDriverOverrides, rest.RestfulTestCase, resp, 'groups', self.assertValidGroup, + keys_to_check=['name', 'description', 'domain_id'], *args, **kwargs) @@ -928,6 +1031,7 @@ class RestfulTestCase(unit.SQLDriverOverrides, rest.RestfulTestCase, resp, 'group', self.assertValidGroup, + keys_to_check=['name', 'description', 'domain_id'], *args, **kwargs) @@ -979,6 +1083,21 @@ class RestfulTestCase(unit.SQLDriverOverrides, rest.RestfulTestCase, *args, **kwargs) + def assertRoleInListResponse(self, resp, ref, expected=1): + found_count = 0 + for entity in resp.result.get('roles'): + try: + self.assertValidRole(entity, ref=ref) + except Exception: + # It doesn't match, so let's go onto the next one + pass + else: + found_count += 1 + self.assertEqual(expected, found_count) + + def assertRoleNotInListResponse(self, resp, ref): + self.assertRoleInListResponse(resp, ref=ref, expected=0) + def assertValidRoleResponse(self, resp, *args, **kwargs): return self.assertValidResponse( resp, @@ -992,6 +1111,7 @@ class RestfulTestCase(unit.SQLDriverOverrides, rest.RestfulTestCase, self.assertIsNotNone(entity.get('name')) if ref: self.assertEqual(ref['name'], entity['name']) + self.assertEqual(ref['domain_id'], entity['domain_id']) return entity # role assignment validation @@ -1161,6 +1281,27 @@ class RestfulTestCase(unit.SQLDriverOverrides, rest.RestfulTestCase, return entity + # Service providers (federation) + + def assertValidServiceProvider(self, entity, ref=None, *args, **kwargs): + + attributes = frozenset(['auth_url', 'id', 'enabled', 'description', + 'links', 'relay_state_prefix', 'sp_url']) + for attribute in attributes: + self.assertIsNotNone(entity.get(attribute)) + + def assertValidServiceProviderListResponse(self, resp, *args, **kwargs): + if kwargs.get('keys_to_check') is None: + kwargs['keys_to_check'] = ['auth_url', 'id', 'enabled', + 'description', 'relay_state_prefix', + 'sp_url'] + return self.assertValidListResponse( + resp, + 'service_providers', + self.assertValidServiceProvider, + *args, + **kwargs) + def build_external_auth_request(self, remote_user, remote_domain=None, auth_data=None, kerberos=False): @@ -1182,24 +1323,81 @@ class VersionTestCase(RestfulTestCase): pass +# NOTE(morganfainberg): To be removed when admin_token_auth is removed. This +# has been split out to allow testing admin_token auth without enabling it +# for other tests. +class AuthContextMiddlewareAdminTokenTestCase(RestfulTestCase): + EXTENSION_TO_ADD = 'admin_token_auth' + + def config_overrides(self): + super(AuthContextMiddlewareAdminTokenTestCase, self).config_overrides() + self.config_fixture.config( + admin_token='ADMIN') + + # NOTE(morganfainberg): This is knowingly copied from below for simplicity + # during the deprecation cycle. + def _middleware_request(self, token, extra_environ=None): + + def application(environ, start_response): + body = b'body' + headers = [('Content-Type', 'text/html; charset=utf8'), + ('Content-Length', str(len(body)))] + start_response('200 OK', headers) + return [body] + + app = webtest.TestApp(middleware.AuthContextMiddleware(application), + extra_environ=extra_environ) + resp = app.get('/', headers={middleware.AUTH_TOKEN_HEADER: token}) + self.assertEqual('body', resp.text) # just to make sure it worked + return resp.request + + def test_admin_auth_context(self): + # test to make sure AuthContextMiddleware does not attempt to build the + # auth context if the admin_token middleware indicates it's admin + # already. + token_id = uuid.uuid4().hex # token doesn't matter. + # the admin_token middleware sets is_admin in the context. + extra_environ = {middleware.CONTEXT_ENV: {'is_admin': True}} + req = self._middleware_request(token_id, extra_environ) + auth_context = req.environ.get(authorization.AUTH_CONTEXT_ENV) + self.assertDictEqual({}, auth_context) + + @mock.patch.object(middleware_auth.versionutils, + 'report_deprecated_feature') + def test_admin_token_auth_context_deprecated(self, mock_report_deprecated): + # For backwards compatibility AuthContextMiddleware will check that the + # admin token (as configured in the CONF file) is present and not + # attempt to build the auth context. This is deprecated. + req = self._middleware_request('ADMIN') + auth_context = req.environ.get(authorization.AUTH_CONTEXT_ENV) + self.assertDictEqual({}, auth_context) + self.assertEqual(1, mock_report_deprecated.call_count) + + # NOTE(gyee): test AuthContextMiddleware here instead of test_middleware.py # because we need the token class AuthContextMiddlewareTestCase(RestfulTestCase): - def _mock_request_object(self, token_id): - class fake_req(object): - headers = {middleware.AUTH_TOKEN_HEADER: token_id} - environ = {} + def _middleware_request(self, token, extra_environ=None): + + def application(environ, start_response): + body = b'body' + headers = [('Content-Type', 'text/html; charset=utf8'), + ('Content-Length', str(len(body)))] + start_response('200 OK', headers) + return [body] - return fake_req() + app = webtest.TestApp(middleware.AuthContextMiddleware(application), + extra_environ=extra_environ) + resp = app.get('/', headers={middleware.AUTH_TOKEN_HEADER: token}) + self.assertEqual(b'body', resp.body) # just to make sure it worked + return resp.request def test_auth_context_build_by_middleware(self): # test to make sure AuthContextMiddleware successful build the auth # context from the incoming auth token admin_token = self.get_scoped_token() - req = self._mock_request_object(admin_token) - application = None - middleware.AuthContextMiddleware(application).process_request(req) + req = self._middleware_request(admin_token) self.assertEqual( self.user['id'], req.environ.get(authorization.AUTH_CONTEXT_ENV)['user_id']) @@ -1208,28 +1406,16 @@ class AuthContextMiddlewareTestCase(RestfulTestCase): overridden_context = 'OVERRIDDEN_CONTEXT' # this token should not be used token = uuid.uuid4().hex - req = self._mock_request_object(token) - req.environ[authorization.AUTH_CONTEXT_ENV] = overridden_context - application = None - middleware.AuthContextMiddleware(application).process_request(req) + + extra_environ = {authorization.AUTH_CONTEXT_ENV: overridden_context} + req = self._middleware_request(token, extra_environ=extra_environ) # make sure overridden context take precedence self.assertEqual(overridden_context, req.environ.get(authorization.AUTH_CONTEXT_ENV)) - def test_admin_token_auth_context(self): - # test to make sure AuthContextMiddleware does not attempt to build - # auth context if the incoming auth token is the special admin token - req = self._mock_request_object(CONF.admin_token) - application = None - middleware.AuthContextMiddleware(application).process_request(req) - self.assertDictEqual(req.environ.get(authorization.AUTH_CONTEXT_ENV), - {}) - def test_unscoped_token_auth_context(self): unscoped_token = self.get_unscoped_token() - req = self._mock_request_object(unscoped_token) - application = None - middleware.AuthContextMiddleware(application).process_request(req) + req = self._middleware_request(unscoped_token) for key in ['project_id', 'domain_id', 'domain_name']: self.assertNotIn( key, @@ -1237,9 +1423,7 @@ class AuthContextMiddlewareTestCase(RestfulTestCase): def test_project_scoped_token_auth_context(self): project_scoped_token = self.get_scoped_token() - req = self._mock_request_object(project_scoped_token) - application = None - middleware.AuthContextMiddleware(application).process_request(req) + req = self._middleware_request(project_scoped_token) self.assertEqual( self.project['id'], req.environ.get(authorization.AUTH_CONTEXT_ENV)['project_id']) @@ -1251,9 +1435,7 @@ class AuthContextMiddlewareTestCase(RestfulTestCase): self.put(path=path) domain_scoped_token = self.get_domain_scoped_token() - req = self._mock_request_object(domain_scoped_token) - application = None - middleware.AuthContextMiddleware(application).process_request(req) + req = self._middleware_request(domain_scoped_token) self.assertEqual( self.domain['id'], req.environ.get(authorization.AUTH_CONTEXT_ENV)['domain_id']) @@ -1261,6 +1443,30 @@ class AuthContextMiddlewareTestCase(RestfulTestCase): self.domain['name'], req.environ.get(authorization.AUTH_CONTEXT_ENV)['domain_name']) + def test_oslo_context(self): + # After AuthContextMiddleware runs, an + # oslo_context.context.RequestContext was created so that its fields + # can be logged. This test validates that the RequestContext was + # created and the fields are set as expected. + + # Use a scoped token so more fields can be set. + token = self.get_scoped_token() + + # oslo_middleware RequestId middleware sets openstack.request_id. + request_id = uuid.uuid4().hex + environ = {'openstack.request_id': request_id} + self._middleware_request(token, extra_environ=environ) + + req_context = oslo_context.context.get_current() + self.assertEqual(request_id, req_context.request_id) + self.assertEqual(token, req_context.auth_token) + self.assertEqual(self.user['id'], req_context.user) + self.assertEqual(self.project['id'], req_context.tenant) + self.assertIsNone(req_context.domain) + self.assertEqual(self.user['domain_id'], req_context.user_domain) + self.assertEqual(self.project['domain_id'], req_context.project_domain) + self.assertFalse(req_context.is_admin) + class JsonHomeTestMixin(object): """JSON Home test @@ -1273,6 +1479,7 @@ class JsonHomeTestMixin(object): data must be in the response. """ + def test_get_json_home(self): resp = self.get('/', convert=False, headers={'Accept': 'application/json-home'}) @@ -1295,7 +1502,6 @@ class AssignmentTestMixin(object): Available filters are: domain_id, project_id, user_id, group_id, role_id and inherited_to_projects. """ - query_params = '?effective' if effective else '' for k, v in filters.items(): @@ -1320,7 +1526,6 @@ class AssignmentTestMixin(object): Provided attributes are expected to contain: domain_id or project_id, user_id or group_id, role_id and, optionally, inherited_to_projects. """ - if attribs.get('domain_id'): link = '/domains/' + attribs['domain_id'] else: @@ -1338,13 +1543,13 @@ class AssignmentTestMixin(object): return link - def build_role_assignment_entity(self, link=None, **attribs): + def build_role_assignment_entity( + self, link=None, prior_role_link=None, **attribs): """Build and return a role assignment entity with provided attributes. Provided attributes are expected to contain: domain_id or project_id, user_id or group_id, role_id and, optionally, inherited_to_projects. """ - entity = {'links': {'assignment': ( link or self.build_role_assignment_link(**attribs))}} @@ -1368,4 +1573,68 @@ class AssignmentTestMixin(object): if attribs.get('inherited_to_projects'): entity['scope']['OS-INHERIT:inherited_to'] = 'projects' + if prior_role_link: + entity['links']['prior_role'] = prior_role_link + + return entity + + def build_role_assignment_entity_include_names(self, + domain_ref=None, + role_ref=None, + group_ref=None, + user_ref=None, + project_ref=None, + inherited_assignment=None): + """Build and return a role assignment entity with provided attributes. + + The expected attributes are: domain_ref or project_ref, + user_ref or group_ref, role_ref and, optionally, inherited_to_projects. + """ + entity = {'links': {}} + attributes_for_links = {} + if project_ref: + dmn_name = self.resource_api.get_domain( + project_ref['domain_id'])['name'] + + entity['scope'] = {'project': { + 'id': project_ref['id'], + 'name': project_ref['name'], + 'domain': { + 'id': project_ref['domain_id'], + 'name': dmn_name}}} + attributes_for_links['project_id'] = project_ref['id'] + else: + entity['scope'] = {'domain': {'id': domain_ref['id'], + 'name': domain_ref['name']}} + attributes_for_links['domain_id'] = domain_ref['id'] + if user_ref: + dmn_name = self.resource_api.get_domain( + user_ref['domain_id'])['name'] + entity['user'] = {'id': user_ref['id'], + 'name': user_ref['name'], + 'domain': {'id': user_ref['domain_id'], + 'name': dmn_name}} + attributes_for_links['user_id'] = user_ref['id'] + else: + dmn_name = self.resource_api.get_domain( + group_ref['domain_id'])['name'] + entity['group'] = {'id': group_ref['id'], + 'name': group_ref['name'], + 'domain': { + 'id': group_ref['domain_id'], + 'name': dmn_name}} + attributes_for_links['group_id'] = group_ref['id'] + + if role_ref: + entity['role'] = {'id': role_ref['id'], + 'name': role_ref['name']} + attributes_for_links['role_id'] = role_ref['id'] + + if inherited_assignment: + entity['scope']['OS-INHERIT:inherited_to'] = 'projects' + attributes_for_links['inherited_to_projects'] = True + + entity['links']['assignment'] = self.build_role_assignment_link( + **attributes_for_links) + return entity diff --git a/keystone-moon/keystone/tests/unit/test_v3_assignment.py b/keystone-moon/keystone/tests/unit/test_v3_assignment.py index 6b15b1c3..86fb9f74 100644 --- a/keystone-moon/keystone/tests/unit/test_v3_assignment.py +++ b/keystone-moon/keystone/tests/unit/test_v3_assignment.py @@ -16,12 +16,10 @@ import uuid from oslo_config import cfg from six.moves import http_client from six.moves import range +from testtools import matchers -from keystone.common import controller -from keystone import exception from keystone.tests import unit from keystone.tests.unit import test_v3 -from keystone.tests.unit import utils CONF = cfg.CONF @@ -29,1042 +27,20 @@ CONF = cfg.CONF class AssignmentTestCase(test_v3.RestfulTestCase, test_v3.AssignmentTestMixin): - """Test domains, projects, roles and role assignments.""" + """Test roles and role assignments.""" def setUp(self): super(AssignmentTestCase, self).setUp() - self.group = self.new_group_ref( - domain_id=self.domain_id) + self.group = unit.new_group_ref(domain_id=self.domain_id) self.group = self.identity_api.create_group(self.group) self.group_id = self.group['id'] - 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) - - # Domain CRUD tests - - def test_create_domain(self): - """Call ``POST /domains``.""" - ref = self.new_domain_ref() - r = self.post( - '/domains', - body={'domain': ref}) - return self.assertValidDomainResponse(r, ref) - - def test_create_domain_case_sensitivity(self): - """Call `POST /domains`` twice with upper() and lower() cased name.""" - ref = self.new_domain_ref() - - # ensure the name is lowercase - ref['name'] = ref['name'].lower() - r = self.post( - '/domains', - body={'domain': ref}) - self.assertValidDomainResponse(r, ref) - - # ensure the name is uppercase - ref['name'] = ref['name'].upper() - r = self.post( - '/domains', - body={'domain': ref}) - self.assertValidDomainResponse(r, ref) - - def test_create_domain_bad_request(self): - """Call ``POST /domains``.""" - self.post('/domains', body={'domain': {}}, - expected_status=http_client.BAD_REQUEST) - - def test_list_domains(self): - """Call ``GET /domains``.""" - resource_url = '/domains' - r = self.get(resource_url) - self.assertValidDomainListResponse(r, ref=self.domain, - resource_url=resource_url) - - def test_get_domain(self): - """Call ``GET /domains/{domain_id}``.""" - r = self.get('/domains/%(domain_id)s' % { - 'domain_id': self.domain_id}) - self.assertValidDomainResponse(r, self.domain) - - def test_update_domain(self): - """Call ``PATCH /domains/{domain_id}``.""" - ref = self.new_domain_ref() - del ref['id'] - r = self.patch('/domains/%(domain_id)s' % { - 'domain_id': self.domain_id}, - body={'domain': ref}) - self.assertValidDomainResponse(r, ref) - - def test_disable_domain(self): - """Call ``PATCH /domains/{domain_id}`` (set enabled=False).""" - # Create a 2nd set of entities in a 2nd domain - self.domain2 = self.new_domain_ref() - self.resource_api.create_domain(self.domain2['id'], self.domain2) - - self.project2 = self.new_project_ref( - domain_id=self.domain2['id']) - self.resource_api.create_project(self.project2['id'], self.project2) - - self.user2 = self.new_user_ref( - domain_id=self.domain2['id'], - project_id=self.project2['id']) - password = self.user2['password'] - self.user2 = self.identity_api.create_user(self.user2) - self.user2['password'] = password - - self.assignment_api.add_user_to_project(self.project2['id'], - self.user2['id']) - - # First check a user in that domain can authenticate. The v2 user - # cannot authenticate because they exist outside the default domain. - body = { - 'auth': { - 'passwordCredentials': { - 'userId': self.user2['id'], - 'password': self.user2['password'] - }, - 'tenantId': self.project2['id'] - } - } - self.admin_request( - path='/v2.0/tokens', method='POST', body=body, - expected_status=http_client.UNAUTHORIZED) - - auth_data = self.build_authentication_request( - user_id=self.user2['id'], - password=self.user2['password'], - project_id=self.project2['id']) - self.v3_authenticate_token(auth_data) - - # Now disable the domain - self.domain2['enabled'] = False - r = self.patch('/domains/%(domain_id)s' % { - 'domain_id': self.domain2['id']}, - body={'domain': {'enabled': False}}) - self.assertValidDomainResponse(r, self.domain2) - - # Make sure the user can no longer authenticate, via - # either API - body = { - 'auth': { - 'passwordCredentials': { - 'userId': self.user2['id'], - 'password': self.user2['password'] - }, - 'tenantId': self.project2['id'] - } - } - self.admin_request( - path='/v2.0/tokens', method='POST', body=body, - expected_status=http_client.UNAUTHORIZED) - - # Try looking up in v3 by name and id - auth_data = self.build_authentication_request( - user_id=self.user2['id'], - password=self.user2['password'], - project_id=self.project2['id']) - self.v3_authenticate_token(auth_data, - expected_status=http_client.UNAUTHORIZED) - - auth_data = self.build_authentication_request( - username=self.user2['name'], - user_domain_id=self.domain2['id'], - password=self.user2['password'], - project_id=self.project2['id']) - self.v3_authenticate_token(auth_data, - expected_status=http_client.UNAUTHORIZED) - - def test_delete_enabled_domain_fails(self): - """Call ``DELETE /domains/{domain_id}`` (when domain enabled).""" - - # Try deleting an enabled domain, which should fail - self.delete('/domains/%(domain_id)s' % { - 'domain_id': self.domain['id']}, - expected_status=exception.ForbiddenAction.code) - - def test_delete_domain(self): - """Call ``DELETE /domains/{domain_id}``. - - The sample data set up already has a user, group, project - and credential that is part of self.domain. Since the user - we will authenticate with is in this domain, we create a - another set of entities in a second domain. Deleting this - second domain should delete all these new entities. In addition, - all the entities in the regular self.domain should be unaffected - by the delete. - - Test Plan: - - - Create domain2 and a 2nd set of entities - - Disable domain2 - - Delete domain2 - - Check entities in domain2 have been deleted - - Check entities in self.domain are unaffected - - """ - - # Create a 2nd set of entities in a 2nd domain - self.domain2 = self.new_domain_ref() - self.resource_api.create_domain(self.domain2['id'], self.domain2) - - self.project2 = self.new_project_ref( - domain_id=self.domain2['id']) - self.resource_api.create_project(self.project2['id'], self.project2) - - self.user2 = self.new_user_ref( - domain_id=self.domain2['id'], - project_id=self.project2['id']) - self.user2 = self.identity_api.create_user(self.user2) - - self.group2 = self.new_group_ref( - domain_id=self.domain2['id']) - self.group2 = self.identity_api.create_group(self.group2) - - self.credential2 = self.new_credential_ref( - user_id=self.user2['id'], - project_id=self.project2['id']) - self.credential_api.create_credential( - self.credential2['id'], - self.credential2) - - # Now disable the new domain and delete it - self.domain2['enabled'] = False - r = self.patch('/domains/%(domain_id)s' % { - 'domain_id': self.domain2['id']}, - body={'domain': {'enabled': False}}) - self.assertValidDomainResponse(r, self.domain2) - self.delete('/domains/%(domain_id)s' % { - 'domain_id': self.domain2['id']}) - - # Check all the domain2 relevant entities are gone - self.assertRaises(exception.DomainNotFound, - self.resource_api.get_domain, - self.domain2['id']) - self.assertRaises(exception.ProjectNotFound, - self.resource_api.get_project, - self.project2['id']) - self.assertRaises(exception.GroupNotFound, - self.identity_api.get_group, - self.group2['id']) - self.assertRaises(exception.UserNotFound, - self.identity_api.get_user, - self.user2['id']) - self.assertRaises(exception.CredentialNotFound, - self.credential_api.get_credential, - self.credential2['id']) - - # ...and that all self.domain entities are still here - r = self.resource_api.get_domain(self.domain['id']) - self.assertDictEqual(r, self.domain) - r = self.resource_api.get_project(self.project['id']) - self.assertDictEqual(r, self.project) - r = self.identity_api.get_group(self.group['id']) - self.assertDictEqual(r, self.group) - r = self.identity_api.get_user(self.user['id']) - self.user.pop('password') - self.assertDictEqual(r, self.user) - r = self.credential_api.get_credential(self.credential['id']) - self.assertDictEqual(r, self.credential) - - def test_delete_default_domain_fails(self): - # Attempting to delete the default domain results in 403 Forbidden. - - # Need to disable it first. - self.patch('/domains/%(domain_id)s' % { - 'domain_id': CONF.identity.default_domain_id}, - body={'domain': {'enabled': False}}) - - self.delete('/domains/%(domain_id)s' % { - 'domain_id': CONF.identity.default_domain_id}, - expected_status=exception.ForbiddenAction.code) - - def test_delete_new_default_domain_fails(self): - # If change the default domain ID, deleting the new default domain - # results in a 403 Forbidden. - - # Create a new domain that's not the default - new_domain = self.new_domain_ref() - new_domain_id = new_domain['id'] - self.resource_api.create_domain(new_domain_id, new_domain) - - # Disable the new domain so can delete it later. - self.patch('/domains/%(domain_id)s' % { - 'domain_id': new_domain_id}, - body={'domain': {'enabled': False}}) - - # Change the default domain - self.config_fixture.config(group='identity', - default_domain_id=new_domain_id) - - # Attempt to delete the new domain - - self.delete('/domains/%(domain_id)s' % {'domain_id': new_domain_id}, - expected_status=exception.ForbiddenAction.code) - - def test_delete_old_default_domain(self): - # If change the default domain ID, deleting the old default domain - # works. - - # Create a new domain that's not the default - new_domain = self.new_domain_ref() - new_domain_id = new_domain['id'] - self.resource_api.create_domain(new_domain_id, new_domain) - - old_default_domain_id = CONF.identity.default_domain_id - - # Disable the default domain so we can delete it later. - self.patch('/domains/%(domain_id)s' % { - 'domain_id': old_default_domain_id}, - body={'domain': {'enabled': False}}) - - # Change the default domain - self.config_fixture.config(group='identity', - default_domain_id=new_domain_id) - - # Delete the old default domain - - self.delete( - '/domains/%(domain_id)s' % {'domain_id': old_default_domain_id}) - - def test_token_revoked_once_domain_disabled(self): - """Test token from a disabled domain has been invalidated. - - Test that a token that was valid for an enabled domain - becomes invalid once that domain is disabled. - - """ - - self.domain = self.new_domain_ref() - self.resource_api.create_domain(self.domain['id'], self.domain) - - self.user2 = self.new_user_ref(domain_id=self.domain['id']) - password = self.user2['password'] - self.user2 = self.identity_api.create_user(self.user2) - self.user2['password'] = password - - # build a request body - auth_body = self.build_authentication_request( - user_id=self.user2['id'], - password=self.user2['password']) - - # sends a request for the user's token - token_resp = self.post('/auth/tokens', body=auth_body) - - subject_token = token_resp.headers.get('x-subject-token') - - # validates the returned token and it should be valid. - self.head('/auth/tokens', - headers={'x-subject-token': subject_token}, - expected_status=200) - - # now disable the domain - self.domain['enabled'] = False - url = "/domains/%(domain_id)s" % {'domain_id': self.domain['id']} - self.patch(url, - body={'domain': {'enabled': False}}, - expected_status=200) - - # validates the same token again and it should be 'not found' - # as the domain has already been disabled. - self.head('/auth/tokens', - headers={'x-subject-token': subject_token}, - expected_status=http_client.NOT_FOUND) - - def test_delete_domain_hierarchy(self): - """Call ``DELETE /domains/{domain_id}``.""" - domain = self.new_domain_ref() - self.resource_api.create_domain(domain['id'], domain) - - root_project = self.new_project_ref( - domain_id=domain['id']) - self.resource_api.create_project(root_project['id'], root_project) - - leaf_project = self.new_project_ref( - domain_id=domain['id'], - parent_id=root_project['id']) - self.resource_api.create_project(leaf_project['id'], leaf_project) - - # Need to disable it first. - self.patch('/domains/%(domain_id)s' % { - 'domain_id': domain['id']}, - body={'domain': {'enabled': False}}) - - self.delete( - '/domains/%(domain_id)s' % { - 'domain_id': domain['id']}) - - self.assertRaises(exception.DomainNotFound, - self.resource_api.get_domain, - domain['id']) - - self.assertRaises(exception.ProjectNotFound, - self.resource_api.get_project, - root_project['id']) - - self.assertRaises(exception.ProjectNotFound, - self.resource_api.get_project, - leaf_project['id']) - - def test_forbid_operations_on_federated_domain(self): - """Make sure one cannot operate on federated domain. - - This includes operations like create, update, delete - on domain identified by id and name where difference variations of - id 'Federated' are used. - - """ - def create_domains(): - for variation in ('Federated', 'FEDERATED', - 'federated', 'fEderated'): - domain = self.new_domain_ref() - domain['id'] = variation - yield domain - - for domain in create_domains(): - self.assertRaises( - AssertionError, self.resource_api.create_domain, - domain['id'], domain) - self.assertRaises( - AssertionError, self.resource_api.update_domain, - domain['id'], domain) - self.assertRaises( - exception.DomainNotFound, self.resource_api.delete_domain, - domain['id']) - - # swap 'name' with 'id' and try again, expecting the request to - # gracefully fail - domain['id'], domain['name'] = domain['name'], domain['id'] - self.assertRaises( - AssertionError, self.resource_api.create_domain, - domain['id'], domain) - self.assertRaises( - AssertionError, self.resource_api.update_domain, - domain['id'], domain) - self.assertRaises( - exception.DomainNotFound, self.resource_api.delete_domain, - domain['id']) - - def test_forbid_operations_on_defined_federated_domain(self): - """Make sure one cannot operate on a user-defined federated domain. - - This includes operations like create, update, delete. - - """ - - non_default_name = 'beta_federated_domain' - self.config_fixture.config(group='federation', - federated_domain_name=non_default_name) - domain = self.new_domain_ref() - domain['name'] = non_default_name - self.assertRaises(AssertionError, - self.resource_api.create_domain, - domain['id'], domain) - self.assertRaises(exception.DomainNotFound, - self.resource_api.delete_domain, - domain['id']) - self.assertRaises(AssertionError, - self.resource_api.update_domain, - domain['id'], domain) - - # Project CRUD tests - - def test_list_projects(self): - """Call ``GET /projects``.""" - resource_url = '/projects' - r = self.get(resource_url) - self.assertValidProjectListResponse(r, ref=self.project, - resource_url=resource_url) - - def test_create_project(self): - """Call ``POST /projects``.""" - ref = self.new_project_ref(domain_id=self.domain_id) - r = self.post( - '/projects', - body={'project': ref}) - self.assertValidProjectResponse(r, ref) - - def test_create_project_bad_request(self): - """Call ``POST /projects``.""" - self.post('/projects', body={'project': {}}, - expected_status=http_client.BAD_REQUEST) - - def test_create_project_invalid_domain_id(self): - """Call ``POST /projects``.""" - ref = self.new_project_ref(domain_id=uuid.uuid4().hex) - self.post('/projects', body={'project': ref}, - expected_status=http_client.BAD_REQUEST) - - def test_create_project_is_domain_not_allowed(self): - """Call ``POST /projects``. - - Setting is_domain=True is not supported yet and should raise - NotImplemented. - - """ - ref = self.new_project_ref(domain_id=self.domain_id, is_domain=True) - self.post('/projects', - body={'project': ref}, - expected_status=501) - - @utils.wip('waiting for projects acting as domains implementation') - def test_create_project_without_parent_id_and_without_domain_id(self): - """Call ``POST /projects``.""" - - # Grant a domain role for the user - collection_url = ( - '/domains/%(domain_id)s/users/%(user_id)s/roles' % { - 'domain_id': self.domain_id, - 'user_id': self.user['id']}) - member_url = '%(collection_url)s/%(role_id)s' % { - 'collection_url': collection_url, - 'role_id': self.role_id} - self.put(member_url) - - # Create an authentication request for a domain scoped token - auth = self.build_authentication_request( - user_id=self.user['id'], - password=self.user['password'], - domain_id=self.domain_id) - - # Without domain_id and parent_id, the domain_id should be - # normalized to the domain on the token, when using a domain - # scoped token. - ref = self.new_project_ref() - r = self.post( - '/projects', - auth=auth, - body={'project': ref}) - ref['domain_id'] = self.domain['id'] - self.assertValidProjectResponse(r, ref) - - @utils.wip('waiting for projects acting as domains implementation') - def test_create_project_with_parent_id_and_no_domain_id(self): - """Call ``POST /projects``.""" - # With only the parent_id, the domain_id should be - # normalized to the parent's domain_id - ref_child = self.new_project_ref(parent_id=self.project['id']) - - r = self.post( - '/projects', - body={'project': ref_child}) - self.assertEqual(r.result['project']['domain_id'], - self.project['domain_id']) - ref_child['domain_id'] = self.domain['id'] - self.assertValidProjectResponse(r, ref_child) - - def _create_projects_hierarchy(self, hierarchy_size=1): - """Creates a single-branched project hierarchy with the specified size. - - :param hierarchy_size: the desired hierarchy size, default is 1 - - a project with one child. - - :returns projects: a list of the projects in the created hierarchy. - - """ - new_ref = self.new_project_ref(domain_id=self.domain_id) - resp = self.post('/projects', body={'project': new_ref}) - - projects = [resp.result] - - for i in range(hierarchy_size): - new_ref = self.new_project_ref( - domain_id=self.domain_id, - parent_id=projects[i]['project']['id']) - resp = self.post('/projects', - body={'project': new_ref}) - self.assertValidProjectResponse(resp, new_ref) - - projects.append(resp.result) - - return projects - - def test_list_projects_filtering_by_parent_id(self): - """Call ``GET /projects?parent_id={project_id}``.""" - projects = self._create_projects_hierarchy(hierarchy_size=2) - - # Add another child to projects[1] - it will be projects[3] - new_ref = self.new_project_ref( - domain_id=self.domain_id, - parent_id=projects[1]['project']['id']) - resp = self.post('/projects', - body={'project': new_ref}) - self.assertValidProjectResponse(resp, new_ref) - - projects.append(resp.result) - - # Query for projects[0] immediate children - it will - # be only projects[1] - r = self.get( - '/projects?parent_id=%(project_id)s' % { - 'project_id': projects[0]['project']['id']}) - self.assertValidProjectListResponse(r) - - projects_result = r.result['projects'] - expected_list = [projects[1]['project']] - - # projects[0] has projects[1] as child - self.assertEqual(expected_list, projects_result) - - # Query for projects[1] immediate children - it will - # be projects[2] and projects[3] - r = self.get( - '/projects?parent_id=%(project_id)s' % { - 'project_id': projects[1]['project']['id']}) - self.assertValidProjectListResponse(r) - - projects_result = r.result['projects'] - expected_list = [projects[2]['project'], projects[3]['project']] - - # projects[1] has projects[2] and projects[3] as children - self.assertEqual(expected_list, projects_result) - - # Query for projects[2] immediate children - it will be an empty list - r = self.get( - '/projects?parent_id=%(project_id)s' % { - 'project_id': projects[2]['project']['id']}) - self.assertValidProjectListResponse(r) - - projects_result = r.result['projects'] - expected_list = [] - - # projects[2] has no child, projects_result must be an empty list - self.assertEqual(expected_list, projects_result) - - def test_create_hierarchical_project(self): - """Call ``POST /projects``.""" - self._create_projects_hierarchy() - - def test_get_project(self): - """Call ``GET /projects/{project_id}``.""" - r = self.get( - '/projects/%(project_id)s' % { - 'project_id': self.project_id}) - self.assertValidProjectResponse(r, self.project) - - def test_get_project_with_parents_as_list_with_invalid_id(self): - """Call ``GET /projects/{project_id}?parents_as_list``.""" - self.get('/projects/%(project_id)s?parents_as_list' % { - 'project_id': None}, expected_status=http_client.NOT_FOUND) - - self.get('/projects/%(project_id)s?parents_as_list' % { - 'project_id': uuid.uuid4().hex}, - expected_status=http_client.NOT_FOUND) - - def test_get_project_with_subtree_as_list_with_invalid_id(self): - """Call ``GET /projects/{project_id}?subtree_as_list``.""" - self.get('/projects/%(project_id)s?subtree_as_list' % { - 'project_id': None}, expected_status=http_client.NOT_FOUND) - - self.get('/projects/%(project_id)s?subtree_as_list' % { - 'project_id': uuid.uuid4().hex}, - expected_status=http_client.NOT_FOUND) - - def test_get_project_with_parents_as_ids(self): - """Call ``GET /projects/{project_id}?parents_as_ids``.""" - projects = self._create_projects_hierarchy(hierarchy_size=2) - - # Query for projects[2] parents_as_ids - r = self.get( - '/projects/%(project_id)s?parents_as_ids' % { - 'project_id': projects[2]['project']['id']}) - - self.assertValidProjectResponse(r, projects[2]['project']) - parents_as_ids = r.result['project']['parents'] - - # Assert parents_as_ids is a structured dictionary correctly - # representing the hierarchy. The request was made using projects[2] - # id, hence its parents should be projects[1] and projects[0]. It - # should have the following structure: - # { - # projects[1]: { - # projects[0]: None - # } - # } - expected_dict = { - projects[1]['project']['id']: { - projects[0]['project']['id']: None - } - } - self.assertDictEqual(expected_dict, parents_as_ids) - - # Query for projects[0] parents_as_ids - r = self.get( - '/projects/%(project_id)s?parents_as_ids' % { - 'project_id': projects[0]['project']['id']}) - - self.assertValidProjectResponse(r, projects[0]['project']) - parents_as_ids = r.result['project']['parents'] - - # projects[0] has no parents, parents_as_ids must be None - self.assertIsNone(parents_as_ids) - - def test_get_project_with_parents_as_list_with_full_access(self): - """``GET /projects/{project_id}?parents_as_list`` with full access. - - Test plan: - - - Create 'parent', 'project' and 'subproject' projects; - - Assign a user a role on each one of those projects; - - Check that calling parents_as_list on 'subproject' returns both - 'project' and 'parent'. - - """ - - # Create the project hierarchy - parent, project, subproject = self._create_projects_hierarchy(2) - - # Assign a role for the user on all the created projects - for proj in (parent, project, subproject): - self.put(self.build_role_assignment_link( - role_id=self.role_id, user_id=self.user_id, - project_id=proj['project']['id'])) - - # Make the API call - r = self.get('/projects/%(project_id)s?parents_as_list' % - {'project_id': subproject['project']['id']}) - self.assertValidProjectResponse(r, subproject['project']) - - # Assert only 'project' and 'parent' are in the parents list - self.assertIn(project, r.result['project']['parents']) - self.assertIn(parent, r.result['project']['parents']) - self.assertEqual(2, len(r.result['project']['parents'])) - - def test_get_project_with_parents_as_list_with_partial_access(self): - """``GET /projects/{project_id}?parents_as_list`` with partial access. - - Test plan: - - - Create 'parent', 'project' and 'subproject' projects; - - Assign a user a role on 'parent' and 'subproject'; - - Check that calling parents_as_list on 'subproject' only returns - 'parent'. - - """ - - # Create the project hierarchy - parent, project, subproject = self._create_projects_hierarchy(2) - - # Assign a role for the user on parent and subproject - for proj in (parent, subproject): - self.put(self.build_role_assignment_link( - role_id=self.role_id, user_id=self.user_id, - project_id=proj['project']['id'])) - - # Make the API call - r = self.get('/projects/%(project_id)s?parents_as_list' % - {'project_id': subproject['project']['id']}) - self.assertValidProjectResponse(r, subproject['project']) - - # Assert only 'parent' is in the parents list - self.assertIn(parent, r.result['project']['parents']) - self.assertEqual(1, len(r.result['project']['parents'])) - - def test_get_project_with_parents_as_list_and_parents_as_ids(self): - """Call ``GET /projects/{project_id}?parents_as_list&parents_as_ids``. - - """ - projects = self._create_projects_hierarchy(hierarchy_size=2) - - self.get( - '/projects/%(project_id)s?parents_as_list&parents_as_ids' % { - 'project_id': projects[1]['project']['id']}, - expected_status=http_client.BAD_REQUEST) - - def test_get_project_with_subtree_as_ids(self): - """Call ``GET /projects/{project_id}?subtree_as_ids``. - - This test creates a more complex hierarchy to test if the structured - dictionary returned by using the ``subtree_as_ids`` query param - correctly represents the hierarchy. - - The hierarchy contains 5 projects with the following structure:: - - +--A--+ - | | - +--B--+ C - | | - D E - - - """ - projects = self._create_projects_hierarchy(hierarchy_size=2) - - # Add another child to projects[0] - it will be projects[3] - new_ref = self.new_project_ref( - domain_id=self.domain_id, - parent_id=projects[0]['project']['id']) - resp = self.post('/projects', - body={'project': new_ref}) - self.assertValidProjectResponse(resp, new_ref) - projects.append(resp.result) - - # Add another child to projects[1] - it will be projects[4] - new_ref = self.new_project_ref( - domain_id=self.domain_id, - parent_id=projects[1]['project']['id']) - resp = self.post('/projects', - body={'project': new_ref}) - self.assertValidProjectResponse(resp, new_ref) - projects.append(resp.result) - - # Query for projects[0] subtree_as_ids - r = self.get( - '/projects/%(project_id)s?subtree_as_ids' % { - 'project_id': projects[0]['project']['id']}) - self.assertValidProjectResponse(r, projects[0]['project']) - subtree_as_ids = r.result['project']['subtree'] - - # The subtree hierarchy from projects[0] should have the following - # structure: - # { - # projects[1]: { - # projects[2]: None, - # projects[4]: None - # }, - # projects[3]: None - # } - expected_dict = { - projects[1]['project']['id']: { - projects[2]['project']['id']: None, - projects[4]['project']['id']: None - }, - projects[3]['project']['id']: None - } - self.assertDictEqual(expected_dict, subtree_as_ids) - - # Now query for projects[1] subtree_as_ids - r = self.get( - '/projects/%(project_id)s?subtree_as_ids' % { - 'project_id': projects[1]['project']['id']}) - self.assertValidProjectResponse(r, projects[1]['project']) - subtree_as_ids = r.result['project']['subtree'] - - # The subtree hierarchy from projects[1] should have the following - # structure: - # { - # projects[2]: None, - # projects[4]: None - # } - expected_dict = { - projects[2]['project']['id']: None, - projects[4]['project']['id']: None - } - self.assertDictEqual(expected_dict, subtree_as_ids) - - # Now query for projects[3] subtree_as_ids - r = self.get( - '/projects/%(project_id)s?subtree_as_ids' % { - 'project_id': projects[3]['project']['id']}) - self.assertValidProjectResponse(r, projects[3]['project']) - subtree_as_ids = r.result['project']['subtree'] - - # projects[3] has no subtree, subtree_as_ids must be None - self.assertIsNone(subtree_as_ids) - - def test_get_project_with_subtree_as_list_with_full_access(self): - """``GET /projects/{project_id}?subtree_as_list`` with full access. - - Test plan: - - - Create 'parent', 'project' and 'subproject' projects; - - Assign a user a role on each one of those projects; - - Check that calling subtree_as_list on 'parent' returns both 'parent' - and 'subproject'. - - """ - - # Create the project hierarchy - parent, project, subproject = self._create_projects_hierarchy(2) - - # Assign a role for the user on all the created projects - for proj in (parent, project, subproject): - self.put(self.build_role_assignment_link( - role_id=self.role_id, user_id=self.user_id, - project_id=proj['project']['id'])) - - # Make the API call - r = self.get('/projects/%(project_id)s?subtree_as_list' % - {'project_id': parent['project']['id']}) - self.assertValidProjectResponse(r, parent['project']) - - # Assert only 'project' and 'subproject' are in the subtree - self.assertIn(project, r.result['project']['subtree']) - self.assertIn(subproject, r.result['project']['subtree']) - self.assertEqual(2, len(r.result['project']['subtree'])) - - def test_get_project_with_subtree_as_list_with_partial_access(self): - """``GET /projects/{project_id}?subtree_as_list`` with partial access. - - Test plan: - - - Create 'parent', 'project' and 'subproject' projects; - - Assign a user a role on 'parent' and 'subproject'; - - Check that calling subtree_as_list on 'parent' returns 'subproject'. - - """ - - # Create the project hierarchy - parent, project, subproject = self._create_projects_hierarchy(2) - - # Assign a role for the user on parent and subproject - for proj in (parent, subproject): - self.put(self.build_role_assignment_link( - role_id=self.role_id, user_id=self.user_id, - project_id=proj['project']['id'])) - - # Make the API call - r = self.get('/projects/%(project_id)s?subtree_as_list' % - {'project_id': parent['project']['id']}) - self.assertValidProjectResponse(r, parent['project']) - - # Assert only 'subproject' is in the subtree - self.assertIn(subproject, r.result['project']['subtree']) - self.assertEqual(1, len(r.result['project']['subtree'])) - - def test_get_project_with_subtree_as_list_and_subtree_as_ids(self): - """Call ``GET /projects/{project_id}?subtree_as_list&subtree_as_ids``. - - """ - projects = self._create_projects_hierarchy(hierarchy_size=2) - - self.get( - '/projects/%(project_id)s?subtree_as_list&subtree_as_ids' % { - 'project_id': projects[1]['project']['id']}, - expected_status=http_client.BAD_REQUEST) - - def test_update_project(self): - """Call ``PATCH /projects/{project_id}``.""" - ref = self.new_project_ref(domain_id=self.domain_id) - del ref['id'] - r = self.patch( - '/projects/%(project_id)s' % { - 'project_id': self.project_id}, - body={'project': ref}) - self.assertValidProjectResponse(r, ref) - - def test_update_project_domain_id(self): - """Call ``PATCH /projects/{project_id}`` with domain_id.""" - project = self.new_project_ref(domain_id=self.domain['id']) - self.resource_api.create_project(project['id'], project) - project['domain_id'] = CONF.identity.default_domain_id - r = self.patch('/projects/%(project_id)s' % { - 'project_id': project['id']}, - body={'project': project}, - expected_status=exception.ValidationError.code) - self.config_fixture.config(domain_id_immutable=False) - project['domain_id'] = self.domain['id'] - r = self.patch('/projects/%(project_id)s' % { - 'project_id': project['id']}, - body={'project': project}) - self.assertValidProjectResponse(r, project) - - def test_update_project_parent_id(self): - """Call ``PATCH /projects/{project_id}``.""" - projects = self._create_projects_hierarchy() - leaf_project = projects[1]['project'] - leaf_project['parent_id'] = None - self.patch( - '/projects/%(project_id)s' % { - 'project_id': leaf_project['id']}, - body={'project': leaf_project}, - expected_status=http_client.FORBIDDEN) - - def test_update_project_is_domain_not_allowed(self): - """Call ``PATCH /projects/{project_id}`` with is_domain. - - The is_domain flag is immutable. - """ - project = self.new_project_ref(domain_id=self.domain['id']) - resp = self.post('/projects', - body={'project': project}) - self.assertFalse(resp.result['project']['is_domain']) - - project['is_domain'] = True - self.patch('/projects/%(project_id)s' % { - 'project_id': resp.result['project']['id']}, - body={'project': project}, - expected_status=http_client.BAD_REQUEST) - - def test_disable_leaf_project(self): - """Call ``PATCH /projects/{project_id}``.""" - projects = self._create_projects_hierarchy() - leaf_project = projects[1]['project'] - leaf_project['enabled'] = False - r = self.patch( - '/projects/%(project_id)s' % { - 'project_id': leaf_project['id']}, - body={'project': leaf_project}) - self.assertEqual( - leaf_project['enabled'], r.result['project']['enabled']) - - def test_disable_not_leaf_project(self): - """Call ``PATCH /projects/{project_id}``.""" - projects = self._create_projects_hierarchy() - root_project = projects[0]['project'] - root_project['enabled'] = False - self.patch( - '/projects/%(project_id)s' % { - 'project_id': root_project['id']}, - body={'project': root_project}, - expected_status=http_client.FORBIDDEN) - - def test_delete_project(self): - """Call ``DELETE /projects/{project_id}`` - - As well as making sure the delete succeeds, we ensure - that any credentials that reference this projects are - also deleted, while other credentials are unaffected. - - """ - # First check the credential for this project is present - 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( - domain_id=self.domain['id']) - self.resource_api.create_project(self.project2['id'], self.project2) - self.credential2 = self.new_credential_ref( - user_id=self.user['id'], - project_id=self.project2['id']) - self.credential_api.create_credential( - self.credential2['id'], - self.credential2) - - # Now delete the project - self.delete( - '/projects/%(project_id)s' % { - 'project_id': self.project_id}) - - # Deleting the project should have deleted any credentials - # that reference this project - self.assertRaises(exception.CredentialNotFound, - self.credential_api.get_credential, - credential_id=self.credential['id']) - # But the credential for project2 is unaffected - r = self.credential_api.get_credential(self.credential2['id']) - self.assertDictEqual(r, self.credential2) - - def test_delete_not_leaf_project(self): - """Call ``DELETE /projects/{project_id}``.""" - projects = self._create_projects_hierarchy() - self.delete( - '/projects/%(project_id)s' % { - 'project_id': projects[0]['project']['id']}, - expected_status=http_client.FORBIDDEN) - # Role CRUD tests def test_create_role(self): """Call ``POST /roles``.""" - ref = self.new_role_ref() + ref = unit.new_role_ref() r = self.post( '/roles', body={'role': ref}) @@ -1090,7 +66,7 @@ class AssignmentTestCase(test_v3.RestfulTestCase, def test_update_role(self): """Call ``PATCH /roles/{role_id}``.""" - ref = self.new_role_ref() + ref = unit.new_role_ref() del ref['id'] r = self.patch('/roles/%(role_id)s' % { 'role_id': self.role_id}, @@ -1105,8 +81,7 @@ class AssignmentTestCase(test_v3.RestfulTestCase, def test_create_member_role(self): """Call ``POST /roles``.""" # specify only the name on creation - ref = self.new_role_ref() - ref['name'] = CONF.member_role_name + ref = unit.new_role_ref(name=CONF.member_role_name) r = self.post( '/roles', body={'role': ref}) @@ -1118,35 +93,41 @@ class AssignmentTestCase(test_v3.RestfulTestCase, # Role Grants tests def test_crud_user_project_role_grants(self): + role = unit.new_role_ref() + self.role_api.create_role(role['id'], role) + collection_url = ( '/projects/%(project_id)s/users/%(user_id)s/roles' % { 'project_id': self.project['id'], 'user_id': self.user['id']}) member_url = '%(collection_url)s/%(role_id)s' % { 'collection_url': collection_url, - 'role_id': self.role_id} + 'role_id': role['id']} + + # There is a role assignment for self.user on self.project + r = self.get(collection_url) + self.assertValidRoleListResponse(r, ref=self.role, + expected_length=1) self.put(member_url) self.head(member_url) r = self.get(collection_url) - self.assertValidRoleListResponse(r, ref=self.role, - resource_url=collection_url) + self.assertValidRoleListResponse(r, ref=role, + resource_url=collection_url, + expected_length=2) - # FIXME(gyee): this test is no longer valid as user - # have no role in the project. Can't get a scoped token - # self.delete(member_url) - # r = self.get(collection_url) - # self.assertValidRoleListResponse(r, expected_length=0) - # self.assertIn(collection_url, r.result['links']['self']) + self.delete(member_url) + r = self.get(collection_url) + self.assertValidRoleListResponse(r, ref=self.role, expected_length=1) + self.assertIn(collection_url, r.result['links']['self']) def test_crud_user_project_role_grants_no_user(self): - """Grant role on a project to a user that doesn't exist, 404 result. + """Grant role on a project to a user that doesn't exist. When grant a role on a project to a user that doesn't exist, the server returns Not Found for the user. """ - user_id = uuid.uuid4().hex collection_url = ( @@ -1179,13 +160,12 @@ class AssignmentTestCase(test_v3.RestfulTestCase, resource_url=collection_url) def test_crud_user_domain_role_grants_no_user(self): - """Grant role on a domain to a user that doesn't exist, 404 result. + """Grant role on a domain to a user that doesn't exist. When grant a role on a domain to a user that doesn't exist, the server returns 404 Not Found for the user. """ - user_id = uuid.uuid4().hex collection_url = ( @@ -1218,13 +198,12 @@ class AssignmentTestCase(test_v3.RestfulTestCase, resource_url=collection_url) def test_crud_group_project_role_grants_no_group(self): - """Grant role on a project to a group that doesn't exist, 404 result. + """Grant role on a project to a group that doesn't exist. When grant a role on a project to a group that doesn't exist, the server returns 404 Not Found for the group. """ - group_id = uuid.uuid4().hex collection_url = ( @@ -1258,13 +237,12 @@ class AssignmentTestCase(test_v3.RestfulTestCase, resource_url=collection_url) def test_crud_group_domain_role_grants_no_group(self): - """Grant role on a domain to a group that doesn't exist, 404 result. + """Grant role on a domain to a group that doesn't exist. When grant a role on a domain to a group that doesn't exist, the server returns 404 Not Found for the group. """ - group_id = uuid.uuid4().hex collection_url = ( @@ -1280,7 +258,7 @@ class AssignmentTestCase(test_v3.RestfulTestCase, def _create_new_user_and_assign_role_on_project(self): """Create a new user and assign user a role on a project.""" # Create a new user - new_user = self.new_user_ref(domain_id=self.domain_id) + new_user = unit.new_user_ref(domain_id=self.domain_id) user_ref = self.identity_api.create_user(new_user) # Assign the user a role on the project collection_url = ( @@ -1290,9 +268,9 @@ class AssignmentTestCase(test_v3.RestfulTestCase, member_url = ('%(collection_url)s/%(role_id)s' % { 'collection_url': collection_url, 'role_id': self.role_id}) - self.put(member_url, expected_status=204) + self.put(member_url) # Check the user has the role assigned - self.head(member_url, expected_status=204) + self.head(member_url) return member_url, user_ref def test_delete_user_before_removing_role_assignment_succeeds(self): @@ -1301,7 +279,7 @@ class AssignmentTestCase(test_v3.RestfulTestCase, # Delete the user from identity backend self.identity_api.driver.delete_user(user['id']) # Clean up the role assignment - self.delete(member_url, expected_status=204) + self.delete(member_url) # Make sure the role is gone self.head(member_url, expected_status=http_client.NOT_FOUND) @@ -1310,8 +288,9 @@ class AssignmentTestCase(test_v3.RestfulTestCase, member_url, user = self._create_new_user_and_assign_role_on_project() # Delete the user from identity backend self.identity_api.delete_user(user['id']) - # We should get a 404 when looking for the user in the identity - # backend because we're not performing a delete operation on the role. + # We should get a 404 Not Found when looking for the user in the + # identity backend because we're not performing a delete operation on + # the role. self.head(member_url, expected_status=http_client.NOT_FOUND) def test_token_revoked_once_group_role_grant_revoked(self): @@ -1344,7 +323,7 @@ class AssignmentTestCase(test_v3.RestfulTestCase, # validates the returned token; it should be valid. self.head('/auth/tokens', headers={'x-subject-token': token}, - expected_status=200) + expected_status=http_client.OK) # revokes the grant from group on project. self.assignment_api.delete_grant(role_id=self.role['id'], @@ -1356,6 +335,126 @@ class AssignmentTestCase(test_v3.RestfulTestCase, headers={'x-subject-token': token}, expected_status=http_client.NOT_FOUND) + @unit.skip_if_cache_disabled('assignment') + def test_delete_grant_from_user_and_project_invalidate_cache(self): + # create a new project + new_project = unit.new_project_ref(domain_id=self.domain_id) + self.resource_api.create_project(new_project['id'], new_project) + + collection_url = ( + '/projects/%(project_id)s/users/%(user_id)s/roles' % { + 'project_id': new_project['id'], + 'user_id': self.user['id']}) + member_url = '%(collection_url)s/%(role_id)s' % { + 'collection_url': collection_url, + 'role_id': self.role_id} + + # create the user a grant on the new project + self.put(member_url) + + # check the grant that was just created + self.head(member_url) + resp = self.get(collection_url) + self.assertValidRoleListResponse(resp, ref=self.role, + resource_url=collection_url) + + # delete the grant + self.delete(member_url) + + # get the collection and ensure there are no roles on the project + resp = self.get(collection_url) + self.assertListEqual(resp.json_body['roles'], []) + + @unit.skip_if_cache_disabled('assignment') + def test_delete_grant_from_user_and_domain_invalidates_cache(self): + # create a new domain + new_domain = unit.new_domain_ref() + self.resource_api.create_domain(new_domain['id'], new_domain) + + collection_url = ( + '/domains/%(domain_id)s/users/%(user_id)s/roles' % { + 'domain_id': new_domain['id'], + 'user_id': self.user['id']}) + member_url = '%(collection_url)s/%(role_id)s' % { + 'collection_url': collection_url, + 'role_id': self.role_id} + + # create the user a grant on the new domain + self.put(member_url) + + # check the grant that was just created + self.head(member_url) + resp = self.get(collection_url) + self.assertValidRoleListResponse(resp, ref=self.role, + resource_url=collection_url) + + # delete the grant + self.delete(member_url) + + # get the collection and ensure there are no roles on the domain + resp = self.get(collection_url) + self.assertListEqual(resp.json_body['roles'], []) + + @unit.skip_if_cache_disabled('assignment') + def test_delete_grant_from_group_and_project_invalidates_cache(self): + # create a new project + new_project = unit.new_project_ref(domain_id=self.domain_id) + self.resource_api.create_project(new_project['id'], new_project) + + collection_url = ( + '/projects/%(project_id)s/groups/%(group_id)s/roles' % { + 'project_id': new_project['id'], + 'group_id': self.group['id']}) + member_url = '%(collection_url)s/%(role_id)s' % { + 'collection_url': collection_url, + 'role_id': self.role_id} + + # create the group a grant on the new project + self.put(member_url) + + # check the grant that was just created + self.head(member_url) + resp = self.get(collection_url) + self.assertValidRoleListResponse(resp, ref=self.role, + resource_url=collection_url) + + # delete the grant + self.delete(member_url) + + # get the collection and ensure there are no roles on the project + resp = self.get(collection_url) + self.assertListEqual(resp.json_body['roles'], []) + + @unit.skip_if_cache_disabled('assignment') + def test_delete_grant_from_group_and_domain_invalidates_cache(self): + # create a new domain + new_domain = unit.new_domain_ref() + self.resource_api.create_domain(new_domain['id'], new_domain) + + collection_url = ( + '/domains/%(domain_id)s/groups/%(group_id)s/roles' % { + 'domain_id': new_domain['id'], + 'group_id': self.group['id']}) + member_url = '%(collection_url)s/%(role_id)s' % { + 'collection_url': collection_url, + 'role_id': self.role_id} + + # create the group a grant on the new domain + self.put(member_url) + + # check the grant that was just created + self.head(member_url) + resp = self.get(collection_url) + self.assertValidRoleListResponse(resp, ref=self.role, + resource_url=collection_url) + + # delete the grant + self.delete(member_url) + + # get the collection and ensure there are no roles on the domain + resp = self.get(collection_url) + self.assertListEqual(resp.json_body['roles'], []) + # Role Assignments tests def test_get_role_assignments(self): @@ -1384,13 +483,11 @@ class AssignmentTestCase(test_v3.RestfulTestCase, been removed """ - # Since the default fixtures already assign some roles to the # user it creates, we also need a new user that will not have any # existing assignments - self.user1 = self.new_user_ref( - domain_id=self.domain['id']) - self.user1 = self.identity_api.create_user(self.user1) + user1 = unit.new_user_ref(domain_id=self.domain['id']) + user1 = self.identity_api.create_user(user1) collection_url = '/role_assignments' r = self.get(collection_url) @@ -1412,7 +509,7 @@ class AssignmentTestCase(test_v3.RestfulTestCase, self.assertRoleAssignmentInListResponse(r, gd_entity) ud_entity = self.build_role_assignment_entity(domain_id=self.domain_id, - user_id=self.user1['id'], + user_id=user1['id'], role_id=self.role_id) self.put(ud_entity['links']['assignment']) r = self.get(collection_url) @@ -1434,7 +531,7 @@ class AssignmentTestCase(test_v3.RestfulTestCase, self.assertRoleAssignmentInListResponse(r, gp_entity) up_entity = self.build_role_assignment_entity( - project_id=self.project_id, user_id=self.user1['id'], + project_id=self.project_id, user_id=user1['id'], role_id=self.role_id) self.put(up_entity['links']['assignment']) r = self.get(collection_url) @@ -1475,18 +572,13 @@ class AssignmentTestCase(test_v3.RestfulTestCase, for each of the group members. """ - self.user1 = self.new_user_ref( - domain_id=self.domain['id']) - password = self.user1['password'] - self.user1 = self.identity_api.create_user(self.user1) - self.user1['password'] = password - self.user2 = self.new_user_ref( - domain_id=self.domain['id']) - password = self.user2['password'] - self.user2 = self.identity_api.create_user(self.user2) - self.user2['password'] = password - self.identity_api.add_user_to_group(self.user1['id'], self.group['id']) - self.identity_api.add_user_to_group(self.user2['id'], self.group['id']) + user1 = unit.create_user(self.identity_api, + domain_id=self.domain['id']) + user2 = unit.create_user(self.identity_api, + domain_id=self.domain['id']) + + self.identity_api.add_user_to_group(user1['id'], self.group['id']) + self.identity_api.add_user_to_group(user2['id'], self.group['id']) collection_url = '/role_assignments' r = self.get(collection_url) @@ -1516,11 +608,11 @@ class AssignmentTestCase(test_v3.RestfulTestCase, resource_url=collection_url) ud_entity = self.build_role_assignment_entity( link=gd_entity['links']['assignment'], domain_id=self.domain_id, - user_id=self.user1['id'], role_id=self.role_id) + user_id=user1['id'], role_id=self.role_id) self.assertRoleAssignmentInListResponse(r, ud_entity) ud_entity = self.build_role_assignment_entity( link=gd_entity['links']['assignment'], domain_id=self.domain_id, - user_id=self.user2['id'], role_id=self.role_id) + user_id=user2['id'], role_id=self.role_id) self.assertRoleAssignmentInListResponse(r, ud_entity) def test_check_effective_values_for_role_assignments(self): @@ -1549,18 +641,13 @@ class AssignmentTestCase(test_v3.RestfulTestCase, know if we are getting effective roles or not """ - self.user1 = self.new_user_ref( - domain_id=self.domain['id']) - password = self.user1['password'] - self.user1 = self.identity_api.create_user(self.user1) - self.user1['password'] = password - self.user2 = self.new_user_ref( - domain_id=self.domain['id']) - password = self.user2['password'] - self.user2 = self.identity_api.create_user(self.user2) - self.user2['password'] = password - self.identity_api.add_user_to_group(self.user1['id'], self.group['id']) - self.identity_api.add_user_to_group(self.user2['id'], self.group['id']) + user1 = unit.create_user(self.identity_api, + domain_id=self.domain['id']) + user2 = unit.create_user(self.identity_api, + domain_id=self.domain['id']) + + self.identity_api.add_user_to_group(user1['id'], self.group['id']) + self.identity_api.add_user_to_group(user2['id'], self.group['id']) collection_url = '/role_assignments' r = self.get(collection_url) @@ -1633,61 +720,53 @@ class AssignmentTestCase(test_v3.RestfulTestCase, token (all effective roles for a user on a project) """ - # Since the default fixtures already assign some roles to the # user it creates, we also need a new user that will not have any # existing assignments - self.user1 = self.new_user_ref( - domain_id=self.domain['id']) - password = self.user1['password'] - self.user1 = self.identity_api.create_user(self.user1) - self.user1['password'] = password - self.user2 = self.new_user_ref( - domain_id=self.domain['id']) - password = self.user2['password'] - self.user2 = self.identity_api.create_user(self.user2) - self.user2['password'] = password - self.group1 = self.new_group_ref( - domain_id=self.domain['id']) - self.group1 = self.identity_api.create_group(self.group1) - self.identity_api.add_user_to_group(self.user1['id'], - self.group1['id']) - self.identity_api.add_user_to_group(self.user2['id'], - self.group1['id']) - self.project1 = self.new_project_ref( - domain_id=self.domain['id']) - self.resource_api.create_project(self.project1['id'], self.project1) - self.role1 = self.new_role_ref() + user1 = unit.create_user(self.identity_api, + domain_id=self.domain['id']) + user2 = unit.create_user(self.identity_api, + domain_id=self.domain['id']) + + group1 = unit.new_group_ref(domain_id=self.domain['id']) + group1 = self.identity_api.create_group(group1) + self.identity_api.add_user_to_group(user1['id'], group1['id']) + self.identity_api.add_user_to_group(user2['id'], group1['id']) + project1 = unit.new_project_ref(domain_id=self.domain['id']) + self.resource_api.create_project(project1['id'], project1) + self.role1 = unit.new_role_ref() self.role_api.create_role(self.role1['id'], self.role1) - self.role2 = self.new_role_ref() + self.role2 = unit.new_role_ref() self.role_api.create_role(self.role2['id'], self.role2) # Now add one of each of the four types of assignment gd_entity = self.build_role_assignment_entity( - domain_id=self.domain_id, group_id=self.group1['id'], + domain_id=self.domain_id, group_id=group1['id'], role_id=self.role1['id']) self.put(gd_entity['links']['assignment']) ud_entity = self.build_role_assignment_entity(domain_id=self.domain_id, - user_id=self.user1['id'], + user_id=user1['id'], role_id=self.role2['id']) self.put(ud_entity['links']['assignment']) gp_entity = self.build_role_assignment_entity( - project_id=self.project1['id'], group_id=self.group1['id'], + project_id=project1['id'], + group_id=group1['id'], role_id=self.role1['id']) self.put(gp_entity['links']['assignment']) up_entity = self.build_role_assignment_entity( - project_id=self.project1['id'], user_id=self.user1['id'], + project_id=project1['id'], + user_id=user1['id'], role_id=self.role2['id']) self.put(up_entity['links']['assignment']) # Now list by various filters to make sure we get back the right ones collection_url = ('/role_assignments?scope.project.id=%s' % - self.project1['id']) + project1['id']) r = self.get(collection_url) self.assertValidRoleAssignmentListResponse(r, expected_length=2, @@ -1704,7 +783,7 @@ class AssignmentTestCase(test_v3.RestfulTestCase, self.assertRoleAssignmentInListResponse(r, ud_entity) self.assertRoleAssignmentInListResponse(r, gd_entity) - collection_url = '/role_assignments?user.id=%s' % self.user1['id'] + collection_url = '/role_assignments?user.id=%s' % user1['id'] r = self.get(collection_url) self.assertValidRoleAssignmentListResponse(r, expected_length=2, @@ -1712,7 +791,7 @@ class AssignmentTestCase(test_v3.RestfulTestCase, self.assertRoleAssignmentInListResponse(r, up_entity) self.assertRoleAssignmentInListResponse(r, ud_entity) - collection_url = '/role_assignments?group.id=%s' % self.group1['id'] + collection_url = '/role_assignments?group.id=%s' % group1['id'] r = self.get(collection_url) self.assertValidRoleAssignmentListResponse(r, expected_length=2, @@ -1733,8 +812,8 @@ class AssignmentTestCase(test_v3.RestfulTestCase, collection_url = ( '/role_assignments?user.id=%(user_id)s' '&scope.project.id=%(project_id)s' % { - 'user_id': self.user1['id'], - 'project_id': self.project1['id']}) + 'user_id': user1['id'], + 'project_id': project1['id']}) r = self.get(collection_url) self.assertValidRoleAssignmentListResponse(r, expected_length=1, @@ -1746,7 +825,7 @@ class AssignmentTestCase(test_v3.RestfulTestCase, # assigned as well as by virtue of group membership collection_url = ('/role_assignments?effective&user.id=%s' % - self.user1['id']) + user1['id']) r = self.get(collection_url) self.assertValidRoleAssignmentListResponse(r, expected_length=4, @@ -1756,17 +835,18 @@ class AssignmentTestCase(test_v3.RestfulTestCase, self.assertRoleAssignmentInListResponse(r, ud_entity) # ...and the two via group membership... gp1_link = self.build_role_assignment_link( - project_id=self.project1['id'], group_id=self.group1['id'], + project_id=project1['id'], + group_id=group1['id'], role_id=self.role1['id']) gd1_link = self.build_role_assignment_link(domain_id=self.domain_id, - group_id=self.group1['id'], + group_id=group1['id'], role_id=self.role1['id']) up1_entity = self.build_role_assignment_entity( - link=gp1_link, project_id=self.project1['id'], - user_id=self.user1['id'], role_id=self.role1['id']) + link=gp1_link, project_id=project1['id'], + user_id=user1['id'], role_id=self.role1['id']) ud1_entity = self.build_role_assignment_entity( - link=gd1_link, domain_id=self.domain_id, user_id=self.user1['id'], + link=gd1_link, domain_id=self.domain_id, user_id=user1['id'], role_id=self.role1['id']) self.assertRoleAssignmentInListResponse(r, up1_entity) self.assertRoleAssignmentInListResponse(r, ud1_entity) @@ -1778,8 +858,8 @@ class AssignmentTestCase(test_v3.RestfulTestCase, collection_url = ( '/role_assignments?effective&user.id=%(user_id)s' '&scope.project.id=%(project_id)s' % { - 'user_id': self.user1['id'], - 'project_id': self.project1['id']}) + 'user_id': user1['id'], + 'project_id': project1['id']}) r = self.get(collection_url) self.assertValidRoleAssignmentListResponse(r, expected_length=2, @@ -1804,7 +884,7 @@ class RoleAssignmentBaseTestCase(test_v3.RestfulTestCase, """ def create_project_hierarchy(parent_id, depth): - "Creates a random project hierarchy." + """Creates a random project hierarchy.""" if depth == 0: return @@ -1812,7 +892,7 @@ class RoleAssignmentBaseTestCase(test_v3.RestfulTestCase, subprojects = [] for i in range(breadth): - subprojects.append(self.new_project_ref( + subprojects.append(unit.new_project_ref( domain_id=self.domain_id, parent_id=parent_id)) self.resource_api.create_project(subprojects[-1]['id'], subprojects[-1]) @@ -1823,12 +903,12 @@ class RoleAssignmentBaseTestCase(test_v3.RestfulTestCase, super(RoleAssignmentBaseTestCase, self).load_sample_data() # Create a domain - self.domain = self.new_domain_ref() + self.domain = unit.new_domain_ref() self.domain_id = self.domain['id'] self.resource_api.create_domain(self.domain_id, self.domain) # Create a project hierarchy - self.project = self.new_project_ref(domain_id=self.domain_id) + self.project = unit.new_project_ref(domain_id=self.domain_id) self.project_id = self.project['id'] self.resource_api.create_project(self.project_id, self.project) @@ -1839,14 +919,14 @@ class RoleAssignmentBaseTestCase(test_v3.RestfulTestCase, # Create 3 users self.user_ids = [] for i in range(3): - user = self.new_user_ref(domain_id=self.domain_id) + user = unit.new_user_ref(domain_id=self.domain_id) user = self.identity_api.create_user(user) self.user_ids.append(user['id']) # Create 3 groups self.group_ids = [] for i in range(3): - group = self.new_group_ref(domain_id=self.domain_id) + group = unit.new_group_ref(domain_id=self.domain_id) group = self.identity_api.create_group(group) self.group_ids.append(group['id']) @@ -1861,7 +941,7 @@ class RoleAssignmentBaseTestCase(test_v3.RestfulTestCase, role_id=self.role_id) # Create a role - self.role = self.new_role_ref() + self.role = unit.new_role_ref() self.role_id = self.role['id'] self.role_api.create_role(self.role_id, self.role) @@ -1869,7 +949,7 @@ class RoleAssignmentBaseTestCase(test_v3.RestfulTestCase, self.default_user_id = self.user_ids[0] self.default_group_id = self.group_ids[0] - def get_role_assignments(self, expected_status=200, **filters): + def get_role_assignments(self, expected_status=http_client.OK, **filters): """Returns the result from querying role assignment API + queried URL. Calls GET /v3/role_assignments? and returns its result, where @@ -1880,7 +960,6 @@ class RoleAssignmentBaseTestCase(test_v3.RestfulTestCase, queried URL. """ - query_url = self._get_role_assignments_query_url(**filters) response = self.get(query_url, expected_status=expected_status) @@ -1903,11 +982,11 @@ class RoleAssignmentBaseTestCase(test_v3.RestfulTestCase, class RoleAssignmentFailureTestCase(RoleAssignmentBaseTestCase): """Class for testing invalid query params on /v3/role_assignments API. - Querying domain and project, or user and group results in a HTTP 400, since - a role assignment must contain only a single pair of (actor, target). In - addition, since filtering on role assignments applies only to the final - result, effective mode cannot be combined with i) group or ii) domain and - inherited, because it would always result in an empty list. + Querying domain and project, or user and group results in a HTTP 400 Bad + Request, since a role assignment must contain only a single pair of (actor, + target). In addition, since filtering on role assignments applies only to + the final result, effective mode cannot be combined with i) group or ii) + domain and inherited, because it would always result in an empty list. """ @@ -1959,7 +1038,6 @@ class RoleAssignmentDirectTestCase(RoleAssignmentBaseTestCase): group_id, user_id and inherited_to_projects. """ - # Fills default assignment with provided filters test_assignment = self._set_default_assignment_attributes(**filters) @@ -2188,10 +1266,7 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase, def test_get_token_from_inherited_user_domain_role_grants(self): # Create a new user to ensure that no grant is loaded from sample data - user = self.new_user_ref(domain_id=self.domain_id) - password = user['password'] - user = self.identity_api.create_user(user) - user['password'] = password + user = unit.create_user(self.identity_api, domain_id=self.domain_id) # Define domain and project authentication data domain_auth_data = self.build_authentication_request( @@ -2204,10 +1279,10 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase, project_id=self.project_id) # Check the user cannot get a domain nor a project token - self.v3_authenticate_token(domain_auth_data, - expected_status=http_client.UNAUTHORIZED) - self.v3_authenticate_token(project_auth_data, - expected_status=http_client.UNAUTHORIZED) + self.v3_create_token(domain_auth_data, + expected_status=http_client.UNAUTHORIZED) + self.v3_create_token(project_auth_data, + expected_status=http_client.UNAUTHORIZED) # Grant non-inherited role for user on domain non_inher_ud_link = self.build_role_assignment_link( @@ -2215,12 +1290,12 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase, self.put(non_inher_ud_link) # Check the user can get only a domain token - self.v3_authenticate_token(domain_auth_data) - self.v3_authenticate_token(project_auth_data, - expected_status=http_client.UNAUTHORIZED) + self.v3_create_token(domain_auth_data) + self.v3_create_token(project_auth_data, + expected_status=http_client.UNAUTHORIZED) # Create inherited role - inherited_role = {'id': uuid.uuid4().hex, 'name': 'inherited'} + inherited_role = unit.new_role_ref(name='inherited') self.role_api.create_role(inherited_role['id'], inherited_role) # Grant inherited role for user on domain @@ -2230,33 +1305,30 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase, self.put(inher_ud_link) # Check the user can get both a domain and a project token - self.v3_authenticate_token(domain_auth_data) - self.v3_authenticate_token(project_auth_data) + self.v3_create_token(domain_auth_data) + self.v3_create_token(project_auth_data) # Delete inherited grant self.delete(inher_ud_link) # Check the user can only get a domain token - self.v3_authenticate_token(domain_auth_data) - self.v3_authenticate_token(project_auth_data, - expected_status=http_client.UNAUTHORIZED) + self.v3_create_token(domain_auth_data) + self.v3_create_token(project_auth_data, + expected_status=http_client.UNAUTHORIZED) # Delete non-inherited grant self.delete(non_inher_ud_link) # Check the user cannot get a domain token anymore - self.v3_authenticate_token(domain_auth_data, - expected_status=http_client.UNAUTHORIZED) + self.v3_create_token(domain_auth_data, + expected_status=http_client.UNAUTHORIZED) def test_get_token_from_inherited_group_domain_role_grants(self): # Create a new group and put a new user in it to # ensure that no grant is loaded from sample data - user = self.new_user_ref(domain_id=self.domain_id) - password = user['password'] - user = self.identity_api.create_user(user) - user['password'] = password + user = unit.create_user(self.identity_api, domain_id=self.domain_id) - group = self.new_group_ref(domain_id=self.domain['id']) + group = unit.new_group_ref(domain_id=self.domain['id']) group = self.identity_api.create_group(group) self.identity_api.add_user_to_group(user['id'], group['id']) @@ -2271,10 +1343,10 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase, project_id=self.project_id) # Check the user cannot get a domain nor a project token - self.v3_authenticate_token(domain_auth_data, - expected_status=http_client.UNAUTHORIZED) - self.v3_authenticate_token(project_auth_data, - expected_status=http_client.UNAUTHORIZED) + self.v3_create_token(domain_auth_data, + expected_status=http_client.UNAUTHORIZED) + self.v3_create_token(project_auth_data, + expected_status=http_client.UNAUTHORIZED) # Grant non-inherited role for user on domain non_inher_gd_link = self.build_role_assignment_link( @@ -2282,12 +1354,12 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase, self.put(non_inher_gd_link) # Check the user can get only a domain token - self.v3_authenticate_token(domain_auth_data) - self.v3_authenticate_token(project_auth_data, - expected_status=http_client.UNAUTHORIZED) + self.v3_create_token(domain_auth_data) + self.v3_create_token(project_auth_data, + expected_status=http_client.UNAUTHORIZED) # Create inherited role - inherited_role = {'id': uuid.uuid4().hex, 'name': 'inherited'} + inherited_role = unit.new_role_ref(name='inherited') self.role_api.create_role(inherited_role['id'], inherited_role) # Grant inherited role for user on domain @@ -2297,27 +1369,27 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase, self.put(inher_gd_link) # Check the user can get both a domain and a project token - self.v3_authenticate_token(domain_auth_data) - self.v3_authenticate_token(project_auth_data) + self.v3_create_token(domain_auth_data) + self.v3_create_token(project_auth_data) # Delete inherited grant self.delete(inher_gd_link) # Check the user can only get a domain token - self.v3_authenticate_token(domain_auth_data) - self.v3_authenticate_token(project_auth_data, - expected_status=http_client.UNAUTHORIZED) + self.v3_create_token(domain_auth_data) + self.v3_create_token(project_auth_data, + expected_status=http_client.UNAUTHORIZED) # Delete non-inherited grant self.delete(non_inher_gd_link) # Check the user cannot get a domain token anymore - self.v3_authenticate_token(domain_auth_data, - expected_status=http_client.UNAUTHORIZED) + self.v3_create_token(domain_auth_data, + expected_status=http_client.UNAUTHORIZED) def _test_crud_inherited_and_direct_assignment_on_target(self, target_url): # Create a new role to avoid assignments loaded from sample data - role = self.new_role_ref() + role = unit.new_role_ref() self.role_api.create_role(role['id'], role) # Define URLs @@ -2360,7 +1432,7 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase, def test_crud_user_inherited_domain_role_grants(self): role_list = [] for _ in range(2): - role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + role = unit.new_role_ref() self.role_api.create_role(role['id'], role) role_list.append(role) @@ -2409,22 +1481,16 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase, """ role_list = [] for _ in range(4): - role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + role = unit.new_role_ref() self.role_api.create_role(role['id'], role) role_list.append(role) - domain = self.new_domain_ref() + domain = unit.new_domain_ref() self.resource_api.create_domain(domain['id'], domain) - user1 = self.new_user_ref( - domain_id=domain['id']) - password = user1['password'] - user1 = self.identity_api.create_user(user1) - user1['password'] = password - project1 = self.new_project_ref( - domain_id=domain['id']) + user1 = unit.create_user(self.identity_api, domain_id=domain['id']) + project1 = unit.new_project_ref(domain_id=domain['id']) self.resource_api.create_project(project1['id'], project1) - project2 = self.new_project_ref( - domain_id=domain['id']) + project2 = unit.new_project_ref(domain_id=domain['id']) self.resource_api.create_project(project2['id'], project2) # Add some roles to the project self.assignment_api.add_role_to_user_and_project( @@ -2490,6 +1556,98 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase, inherited_to_projects=True) self.assertRoleAssignmentInListResponse(r, up_entity) + def test_list_role_assignments_include_names(self): + """Call ``GET /role_assignments with include names``. + + Test Plan: + + - Create a domain with a group and a user + - Create a project with a group and a user + + """ + role1 = unit.new_role_ref() + self.role_api.create_role(role1['id'], role1) + user1 = unit.create_user(self.identity_api, domain_id=self.domain_id) + group = unit.new_group_ref(domain_id=self.domain_id) + group = self.identity_api.create_group(group) + project1 = unit.new_project_ref(domain_id=self.domain_id) + self.resource_api.create_project(project1['id'], project1) + + expected_entity1 = self.build_role_assignment_entity_include_names( + role_ref=role1, + project_ref=project1, + user_ref=user1) + self.put(expected_entity1['links']['assignment']) + expected_entity2 = self.build_role_assignment_entity_include_names( + role_ref=role1, + domain_ref=self.domain, + group_ref=group) + self.put(expected_entity2['links']['assignment']) + expected_entity3 = self.build_role_assignment_entity_include_names( + role_ref=role1, + domain_ref=self.domain, + user_ref=user1) + self.put(expected_entity3['links']['assignment']) + expected_entity4 = self.build_role_assignment_entity_include_names( + role_ref=role1, + project_ref=project1, + group_ref=group) + self.put(expected_entity4['links']['assignment']) + + collection_url_domain = ( + '/role_assignments?include_names&scope.domain.id=%(domain_id)s' % { + 'domain_id': self.domain_id}) + rs_domain = self.get(collection_url_domain) + collection_url_project = ( + '/role_assignments?include_names&' + 'scope.project.id=%(project_id)s' % { + 'project_id': project1['id']}) + rs_project = self.get(collection_url_project) + collection_url_group = ( + '/role_assignments?include_names&group.id=%(group_id)s' % { + 'group_id': group['id']}) + rs_group = self.get(collection_url_group) + collection_url_user = ( + '/role_assignments?include_names&user.id=%(user_id)s' % { + 'user_id': user1['id']}) + rs_user = self.get(collection_url_user) + collection_url_role = ( + '/role_assignments?include_names&role.id=%(role_id)s' % { + 'role_id': role1['id']}) + rs_role = self.get(collection_url_role) + # Make sure all entities were created successfully + self.assertEqual(rs_domain.status_int, http_client.OK) + self.assertEqual(rs_project.status_int, http_client.OK) + self.assertEqual(rs_group.status_int, http_client.OK) + self.assertEqual(rs_user.status_int, http_client.OK) + # Make sure we can get back the correct number of entities + self.assertValidRoleAssignmentListResponse( + rs_domain, + expected_length=2, + resource_url=collection_url_domain) + self.assertValidRoleAssignmentListResponse( + rs_project, + expected_length=2, + resource_url=collection_url_project) + self.assertValidRoleAssignmentListResponse( + rs_group, + expected_length=2, + resource_url=collection_url_group) + self.assertValidRoleAssignmentListResponse( + rs_user, + expected_length=2, + resource_url=collection_url_user) + self.assertValidRoleAssignmentListResponse( + rs_role, + expected_length=4, + resource_url=collection_url_role) + # Verify all types of entities have the correct format + self.assertRoleAssignmentInListResponse(rs_domain, expected_entity2) + self.assertRoleAssignmentInListResponse(rs_project, expected_entity1) + self.assertRoleAssignmentInListResponse(rs_group, expected_entity4) + self.assertRoleAssignmentInListResponse(rs_user, expected_entity3) + self.assertRoleAssignmentInListResponse(rs_role, expected_entity1) + def test_list_role_assignments_for_disabled_inheritance_extension(self): """Call ``GET /role_assignments with inherited domain grants``. @@ -2503,25 +1661,18 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase, shows up. """ - role_list = [] for _ in range(4): - role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + role = unit.new_role_ref() self.role_api.create_role(role['id'], role) role_list.append(role) - domain = self.new_domain_ref() + domain = unit.new_domain_ref() self.resource_api.create_domain(domain['id'], domain) - user1 = self.new_user_ref( - domain_id=domain['id']) - password = user1['password'] - user1 = self.identity_api.create_user(user1) - user1['password'] = password - project1 = self.new_project_ref( - domain_id=domain['id']) + user1 = unit.create_user(self.identity_api, domain_id=domain['id']) + project1 = unit.new_project_ref(domain_id=domain['id']) self.resource_api.create_project(project1['id'], project1) - project2 = self.new_project_ref( - domain_id=domain['id']) + project2 = unit.new_project_ref(domain_id=domain['id']) self.resource_api.create_project(project2['id'], project2) # Add some roles to the project self.assignment_api.add_role_to_user_and_project( @@ -2598,34 +1749,23 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase, """ role_list = [] for _ in range(4): - role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + role = unit.new_role_ref() self.role_api.create_role(role['id'], role) role_list.append(role) - domain = self.new_domain_ref() + domain = unit.new_domain_ref() self.resource_api.create_domain(domain['id'], domain) - user1 = self.new_user_ref( - domain_id=domain['id']) - password = user1['password'] - user1 = self.identity_api.create_user(user1) - user1['password'] = password - user2 = self.new_user_ref( - domain_id=domain['id']) - password = user2['password'] - user2 = self.identity_api.create_user(user2) - user2['password'] = password - group1 = self.new_group_ref( - domain_id=domain['id']) + user1 = unit.create_user(self.identity_api, domain_id=domain['id']) + user2 = unit.create_user(self.identity_api, domain_id=domain['id']) + group1 = unit.new_group_ref(domain_id=domain['id']) group1 = self.identity_api.create_group(group1) self.identity_api.add_user_to_group(user1['id'], group1['id']) self.identity_api.add_user_to_group(user2['id'], group1['id']) - project1 = self.new_project_ref( - domain_id=domain['id']) + project1 = unit.new_project_ref(domain_id=domain['id']) self.resource_api.create_project(project1['id'], project1) - project2 = self.new_project_ref( - domain_id=domain['id']) + project2 = unit.new_project_ref(domain_id=domain['id']) self.resource_api.create_project(project2['id'], project2) # Add some roles to the project self.assignment_api.add_role_to_user_and_project( @@ -2704,25 +1844,18 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase, """ role_list = [] for _ in range(5): - role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + role = unit.new_role_ref() self.role_api.create_role(role['id'], role) role_list.append(role) - domain = self.new_domain_ref() + domain = unit.new_domain_ref() self.resource_api.create_domain(domain['id'], domain) - user1 = self.new_user_ref( - domain_id=domain['id']) - password = user1['password'] - user1 = self.identity_api.create_user(user1) - user1['password'] = password - group1 = self.new_group_ref( - domain_id=domain['id']) + user1 = unit.create_user(self.identity_api, domain_id=domain['id']) + group1 = unit.new_group_ref(domain_id=domain['id']) group1 = self.identity_api.create_group(group1) - project1 = self.new_project_ref( - domain_id=domain['id']) + project1 = unit.new_project_ref(domain_id=domain['id']) self.resource_api.create_project(project1['id'], project1) - project2 = self.new_project_ref( - domain_id=domain['id']) + project2 = unit.new_project_ref(domain_id=domain['id']) self.resource_api.create_project(project2['id'], project2) # Add some spoiler roles to the projects self.assignment_api.add_role_to_user_and_project( @@ -2790,17 +1923,17 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase, """ # Create project hierarchy - root = self.new_project_ref(domain_id=self.domain['id']) - leaf = self.new_project_ref(domain_id=self.domain['id'], + root = unit.new_project_ref(domain_id=self.domain['id']) + leaf = unit.new_project_ref(domain_id=self.domain['id'], parent_id=root['id']) self.resource_api.create_project(root['id'], root) self.resource_api.create_project(leaf['id'], leaf) # Create 'non-inherited' and 'inherited' roles - non_inherited_role = {'id': uuid.uuid4().hex, 'name': 'non-inherited'} + non_inherited_role = unit.new_role_ref(name='non-inherited') self.role_api.create_role(non_inherited_role['id'], non_inherited_role) - inherited_role = {'id': uuid.uuid4().hex, 'name': 'inherited'} + inherited_role = unit.new_role_ref(name='inherited') self.role_api.create_role(inherited_role['id'], inherited_role) return (root['id'], leaf['id'], @@ -2822,10 +1955,10 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase, project_id=leaf_id) # Check the user cannot get a token on root nor leaf project - self.v3_authenticate_token(root_project_auth_data, - expected_status=http_client.UNAUTHORIZED) - self.v3_authenticate_token(leaf_project_auth_data, - expected_status=http_client.UNAUTHORIZED) + self.v3_create_token(root_project_auth_data, + expected_status=http_client.UNAUTHORIZED) + self.v3_create_token(leaf_project_auth_data, + expected_status=http_client.UNAUTHORIZED) # Grant non-inherited role for user on leaf project non_inher_up_link = self.build_role_assignment_link( @@ -2834,9 +1967,9 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase, self.put(non_inher_up_link) # Check the user can only get a token on leaf project - self.v3_authenticate_token(root_project_auth_data, - expected_status=http_client.UNAUTHORIZED) - self.v3_authenticate_token(leaf_project_auth_data) + self.v3_create_token(root_project_auth_data, + expected_status=http_client.UNAUTHORIZED) + self.v3_create_token(leaf_project_auth_data) # Grant inherited role for user on root project inher_up_link = self.build_role_assignment_link( @@ -2845,24 +1978,24 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase, self.put(inher_up_link) # Check the user still can get a token only on leaf project - self.v3_authenticate_token(root_project_auth_data, - expected_status=http_client.UNAUTHORIZED) - self.v3_authenticate_token(leaf_project_auth_data) + self.v3_create_token(root_project_auth_data, + expected_status=http_client.UNAUTHORIZED) + self.v3_create_token(leaf_project_auth_data) # Delete non-inherited grant self.delete(non_inher_up_link) # Check the inherited role still applies for leaf project - self.v3_authenticate_token(root_project_auth_data, - expected_status=http_client.UNAUTHORIZED) - self.v3_authenticate_token(leaf_project_auth_data) + self.v3_create_token(root_project_auth_data, + expected_status=http_client.UNAUTHORIZED) + self.v3_create_token(leaf_project_auth_data) # Delete inherited grant self.delete(inher_up_link) # Check the user cannot get a token on leaf project anymore - self.v3_authenticate_token(leaf_project_auth_data, - expected_status=http_client.UNAUTHORIZED) + self.v3_create_token(leaf_project_auth_data, + expected_status=http_client.UNAUTHORIZED) def test_get_token_from_inherited_group_project_role_grants(self): # Create default scenario @@ -2870,7 +2003,7 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase, self._setup_hierarchical_projects_scenario()) # Create group and add user to it - group = self.new_group_ref(domain_id=self.domain['id']) + group = unit.new_group_ref(domain_id=self.domain['id']) group = self.identity_api.create_group(group) self.identity_api.add_user_to_group(self.user['id'], group['id']) @@ -2885,10 +2018,10 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase, project_id=leaf_id) # Check the user cannot get a token on root nor leaf project - self.v3_authenticate_token(root_project_auth_data, - expected_status=http_client.UNAUTHORIZED) - self.v3_authenticate_token(leaf_project_auth_data, - expected_status=http_client.UNAUTHORIZED) + self.v3_create_token(root_project_auth_data, + expected_status=http_client.UNAUTHORIZED) + self.v3_create_token(leaf_project_auth_data, + expected_status=http_client.UNAUTHORIZED) # Grant non-inherited role for group on leaf project non_inher_gp_link = self.build_role_assignment_link( @@ -2897,9 +2030,9 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase, self.put(non_inher_gp_link) # Check the user can only get a token on leaf project - self.v3_authenticate_token(root_project_auth_data, - expected_status=http_client.UNAUTHORIZED) - self.v3_authenticate_token(leaf_project_auth_data) + self.v3_create_token(root_project_auth_data, + expected_status=http_client.UNAUTHORIZED) + self.v3_create_token(leaf_project_auth_data) # Grant inherited role for group on root project inher_gp_link = self.build_role_assignment_link( @@ -2908,22 +2041,22 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase, self.put(inher_gp_link) # Check the user still can get a token only on leaf project - self.v3_authenticate_token(root_project_auth_data, - expected_status=http_client.UNAUTHORIZED) - self.v3_authenticate_token(leaf_project_auth_data) + self.v3_create_token(root_project_auth_data, + expected_status=http_client.UNAUTHORIZED) + self.v3_create_token(leaf_project_auth_data) # Delete no-inherited grant self.delete(non_inher_gp_link) # Check the inherited role still applies for leaf project - self.v3_authenticate_token(leaf_project_auth_data) + self.v3_create_token(leaf_project_auth_data) # Delete inherited grant self.delete(inher_gp_link) # Check the user cannot get a token on leaf project anymore - self.v3_authenticate_token(leaf_project_auth_data, - expected_status=http_client.UNAUTHORIZED) + self.v3_create_token(leaf_project_auth_data, + expected_status=http_client.UNAUTHORIZED) def test_get_role_assignments_for_project_hierarchy(self): """Call ``GET /role_assignments``. @@ -3028,6 +2161,154 @@ class AssignmentInheritanceTestCase(test_v3.RestfulTestCase, inher_up_entity['scope']['project']['id'] = leaf_id self.assertRoleAssignmentInListResponse(r, inher_up_entity) + def test_project_id_specified_if_include_subtree_specified(self): + """When using include_subtree, you must specify a project ID.""" + self.get('/role_assignments?include_subtree=True', + expected_status=http_client.BAD_REQUEST) + self.get('/role_assignments?scope.project.id&' + 'include_subtree=True', + expected_status=http_client.BAD_REQUEST) + + def test_get_role_assignments_for_project_tree(self): + """Get role_assignment?scope.project.id=X?include_subtree``. + + Test Plan: + + - Create 2 roles and a hierarchy of projects with one root and one leaf + - Issue the URL to add a non-inherited user role to the root project + and the leaf project + - Issue the URL to get role assignments for the root project but + not the subtree - this should return just the root assignment + - Issue the URL to get role assignments for the root project and + it's subtree - this should return both assignments + - Check that explicitly setting include_subtree to False is the + equivalent to not including it at all in the query. + + """ + # Create default scenario + root_id, leaf_id, non_inherited_role_id, unused_role_id = ( + self._setup_hierarchical_projects_scenario()) + + # Grant non-inherited role to root and leaf projects + non_inher_entity_root = self.build_role_assignment_entity( + project_id=root_id, user_id=self.user['id'], + role_id=non_inherited_role_id) + self.put(non_inher_entity_root['links']['assignment']) + non_inher_entity_leaf = self.build_role_assignment_entity( + project_id=leaf_id, user_id=self.user['id'], + role_id=non_inherited_role_id) + self.put(non_inher_entity_leaf['links']['assignment']) + + # Without the subtree, we should get the one assignment on the + # root project + collection_url = ( + '/role_assignments?scope.project.id=%(project)s' % { + 'project': root_id}) + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse( + r, resource_url=collection_url) + + self.assertThat(r.result['role_assignments'], matchers.HasLength(1)) + self.assertRoleAssignmentInListResponse(r, non_inher_entity_root) + + # With the subtree, we should get both assignments + collection_url = ( + '/role_assignments?scope.project.id=%(project)s' + '&include_subtree=True' % { + 'project': root_id}) + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse( + r, resource_url=collection_url) + + self.assertThat(r.result['role_assignments'], matchers.HasLength(2)) + self.assertRoleAssignmentInListResponse(r, non_inher_entity_root) + self.assertRoleAssignmentInListResponse(r, non_inher_entity_leaf) + + # With subtree=0, we should also only get the one assignment on the + # root project + collection_url = ( + '/role_assignments?scope.project.id=%(project)s' + '&include_subtree=0' % { + 'project': root_id}) + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse( + r, resource_url=collection_url) + + self.assertThat(r.result['role_assignments'], matchers.HasLength(1)) + self.assertRoleAssignmentInListResponse(r, non_inher_entity_root) + + def test_get_effective_role_assignments_for_project_tree(self): + """Get role_assignment ?project_id=X?include_subtree=True?effective``. + + Test Plan: + + - Create 2 roles and a hierarchy of projects with one root and 4 levels + of child project + - Issue the URL to add a non-inherited user role to the root project + and a level 1 project + - Issue the URL to add an inherited user role on the level 2 project + - Issue the URL to get effective role assignments for the level 1 + project and it's subtree - this should return a role (non-inherited) + on the level 1 project and roles (inherited) on each of the level + 2, 3 and 4 projects + + """ + # Create default scenario + root_id, leaf_id, non_inherited_role_id, inherited_role_id = ( + self._setup_hierarchical_projects_scenario()) + + # Add some extra projects to the project hierarchy + level2 = unit.new_project_ref(domain_id=self.domain['id'], + parent_id=leaf_id) + level3 = unit.new_project_ref(domain_id=self.domain['id'], + parent_id=level2['id']) + level4 = unit.new_project_ref(domain_id=self.domain['id'], + parent_id=level3['id']) + self.resource_api.create_project(level2['id'], level2) + self.resource_api.create_project(level3['id'], level3) + self.resource_api.create_project(level4['id'], level4) + + # Grant non-inherited role to root (as a spoiler) and to + # the level 1 (leaf) project + non_inher_entity_root = self.build_role_assignment_entity( + project_id=root_id, user_id=self.user['id'], + role_id=non_inherited_role_id) + self.put(non_inher_entity_root['links']['assignment']) + non_inher_entity_leaf = self.build_role_assignment_entity( + project_id=leaf_id, user_id=self.user['id'], + role_id=non_inherited_role_id) + self.put(non_inher_entity_leaf['links']['assignment']) + + # Grant inherited role to level 2 + inher_entity = self.build_role_assignment_entity( + project_id=level2['id'], user_id=self.user['id'], + role_id=inherited_role_id, inherited_to_projects=True) + self.put(inher_entity['links']['assignment']) + + # Get effective role assignments + collection_url = ( + '/role_assignments?scope.project.id=%(project)s' + '&include_subtree=True&effective' % { + 'project': leaf_id}) + r = self.get(collection_url) + self.assertValidRoleAssignmentListResponse( + r, resource_url=collection_url) + + # There should be three assignments returned in total + self.assertThat(r.result['role_assignments'], matchers.HasLength(3)) + + # Assert that the user does not non-inherited role on root project + self.assertRoleAssignmentNotInListResponse(r, non_inher_entity_root) + + # Assert that the user does have non-inherited role on leaf project + self.assertRoleAssignmentInListResponse(r, non_inher_entity_leaf) + + # Assert that the user has inherited role on levels 3 and 4 + inher_entity['scope']['project']['id'] = level3['id'] + self.assertRoleAssignmentInListResponse(r, inher_entity) + inher_entity['scope']['project']['id'] = level4['id'] + self.assertRoleAssignmentInListResponse(r, inher_entity) + def test_get_inherited_role_assignments_for_project_hierarchy(self): """Call ``GET /role_assignments?scope.OS-INHERIT:inherited_to``. @@ -3089,7 +2370,7 @@ class AssignmentInheritanceDisabledTestCase(test_v3.RestfulTestCase): self.config_fixture.config(group='os_inherit', enabled=False) def test_crud_inherited_role_grants_failed_if_disabled(self): - role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + role = unit.new_role_ref() self.role_api.create_role(role['id'], role) base_collection_url = ( @@ -3107,118 +2388,484 @@ class AssignmentInheritanceDisabledTestCase(test_v3.RestfulTestCase): self.delete(member_url, expected_status=http_client.NOT_FOUND) -class AssignmentV3toV2MethodsTestCase(unit.TestCase): - """Test domain V3 to V2 conversion methods.""" - def _setup_initial_projects(self): - self.project_id = uuid.uuid4().hex - self.domain_id = CONF.identity.default_domain_id - self.parent_id = uuid.uuid4().hex - # Project with only domain_id in ref - self.project1 = {'id': self.project_id, - 'name': self.project_id, - 'domain_id': self.domain_id} - # Project with both domain_id and parent_id in ref - self.project2 = {'id': self.project_id, - 'name': self.project_id, - 'domain_id': self.domain_id, - 'parent_id': self.parent_id} - # Project with no domain_id and parent_id in ref - self.project3 = {'id': self.project_id, - 'name': self.project_id, - 'domain_id': self.domain_id, - 'parent_id': self.parent_id} - # Expected result with no domain_id and parent_id - self.expected_project = {'id': self.project_id, - 'name': self.project_id} - - def test_v2controller_filter_domain_id(self): - # V2.0 is not domain aware, ensure domain_id is popped off the ref. - other_data = uuid.uuid4().hex - domain_id = CONF.identity.default_domain_id - ref = {'domain_id': domain_id, - 'other_data': other_data} - - ref_no_domain = {'other_data': other_data} - expected_ref = ref_no_domain.copy() - - updated_ref = controller.V2Controller.filter_domain_id(ref) - self.assertIs(ref, updated_ref) - self.assertDictEqual(ref, expected_ref) - # Make sure we don't error/muck up data if domain_id isn't present - updated_ref = controller.V2Controller.filter_domain_id(ref_no_domain) - self.assertIs(ref_no_domain, updated_ref) - self.assertDictEqual(ref_no_domain, expected_ref) - - def test_v3controller_filter_domain_id(self): - # No data should be filtered out in this case. - other_data = uuid.uuid4().hex - domain_id = uuid.uuid4().hex - ref = {'domain_id': domain_id, - 'other_data': other_data} - - expected_ref = ref.copy() - updated_ref = controller.V3Controller.filter_domain_id(ref) - self.assertIs(ref, updated_ref) - self.assertDictEqual(ref, expected_ref) - - def test_v2controller_filter_domain(self): - other_data = uuid.uuid4().hex - domain_id = uuid.uuid4().hex - non_default_domain_ref = {'domain': {'id': domain_id}, - 'other_data': other_data} - default_domain_ref = {'domain': {'id': 'default'}, - 'other_data': other_data} - updated_ref = controller.V2Controller.filter_domain(default_domain_ref) - self.assertNotIn('domain', updated_ref) - self.assertRaises(exception.Unauthorized, - controller.V2Controller.filter_domain, - non_default_domain_ref) - - def test_v2controller_filter_project_parent_id(self): - # V2.0 is not project hierarchy aware, ensure parent_id is popped off. - other_data = uuid.uuid4().hex - parent_id = uuid.uuid4().hex - ref = {'parent_id': parent_id, - 'other_data': other_data} - - ref_no_parent = {'other_data': other_data} - expected_ref = ref_no_parent.copy() - - updated_ref = controller.V2Controller.filter_project_parent_id(ref) - self.assertIs(ref, updated_ref) - self.assertDictEqual(ref, expected_ref) - # Make sure we don't error/muck up data if parent_id isn't present - updated_ref = controller.V2Controller.filter_project_parent_id( - ref_no_parent) - self.assertIs(ref_no_parent, updated_ref) - self.assertDictEqual(ref_no_parent, expected_ref) - - def test_v3_to_v2_project_method(self): - self._setup_initial_projects() - updated_project1 = controller.V2Controller.v3_to_v2_project( - self.project1) - self.assertIs(self.project1, updated_project1) - self.assertDictEqual(self.project1, self.expected_project) - updated_project2 = controller.V2Controller.v3_to_v2_project( - self.project2) - self.assertIs(self.project2, updated_project2) - self.assertDictEqual(self.project2, self.expected_project) - updated_project3 = controller.V2Controller.v3_to_v2_project( - self.project3) - self.assertIs(self.project3, updated_project3) - self.assertDictEqual(self.project3, self.expected_project) - - def test_v3_to_v2_project_method_list(self): - self._setup_initial_projects() - project_list = [self.project1, self.project2, self.project3] - updated_list = controller.V2Controller.v3_to_v2_project(project_list) - - self.assertEqual(len(updated_list), len(project_list)) - - for i, ref in enumerate(updated_list): - # Order should not change. - self.assertIs(ref, project_list[i]) - - self.assertDictEqual(self.project1, self.expected_project) - self.assertDictEqual(self.project2, self.expected_project) - self.assertDictEqual(self.project3, self.expected_project) +class ImpliedRolesTests(test_v3.RestfulTestCase, test_v3.AssignmentTestMixin, + unit.TestCase): + def _create_role(self): + """Call ``POST /roles``.""" + ref = unit.new_role_ref() + r = self.post('/roles', body={'role': ref}) + return self.assertValidRoleResponse(r, ref) + + def test_list_implied_roles_none(self): + self.prior = self._create_role() + url = '/roles/%s/implies' % (self.prior['id']) + response = self.get(url).json["role_inference"] + self.assertEqual(self.prior['id'], response['prior_role']['id']) + self.assertEqual(0, len(response['implies'])) + + def _create_implied_role(self, prior, implied): + self.put('/roles/%s/implies/%s' % (prior['id'], implied['id']), + expected_status=http_client.CREATED) + + def _delete_implied_role(self, prior, implied): + self.delete('/roles/%s/implies/%s' % (prior['id'], implied['id'])) + + def _setup_prior_two_implied(self): + self.prior = self._create_role() + self.implied1 = self._create_role() + self._create_implied_role(self.prior, self.implied1) + self.implied2 = self._create_role() + self._create_implied_role(self.prior, self.implied2) + + def _assert_expected_implied_role_response( + self, expected_prior_id, expected_implied_ids): + r = self.get('/roles/%s/implies' % expected_prior_id) + response = r.json["role_inference"] + self.assertEqual(expected_prior_id, response['prior_role']['id']) + + actual_implied_ids = [implied['id'] for implied in response['implies']] + + for expected_id in expected_implied_ids: + self.assertIn(expected_id, actual_implied_ids) + self.assertEqual(len(expected_implied_ids), len(response['implies'])) + + self.assertIsNotNone(response['prior_role']['links']['self']) + for implied in response['implies']: + self.assertIsNotNone(implied['links']['self']) + + def _assert_two_roles_implied(self): + self._assert_expected_implied_role_response( + self.prior['id'], [self.implied1['id'], self.implied2['id']]) + + def _assert_one_role_implied(self): + self._assert_expected_implied_role_response( + self.prior['id'], [self.implied1['id']]) + + self.get('/roles/%s/implies/%s' % + (self.prior['id'], self.implied2['id']), + expected_status=http_client.NOT_FOUND) + + def _assert_two_rules_defined(self): + r = self.get('/role_inferences/') + + rules = r.result['role_inferences'] + + self.assertEqual(self.prior['id'], rules[0]['prior_role']['id']) + self.assertEqual(2, len(rules[0]['implies'])) + implied_ids = [implied['id'] for implied in rules[0]['implies']] + implied_names = [implied['name'] for implied in rules[0]['implies']] + + self.assertIn(self.implied1['id'], implied_ids) + self.assertIn(self.implied2['id'], implied_ids) + self.assertIn(self.implied1['name'], implied_names) + self.assertIn(self.implied2['name'], implied_names) + + def _assert_one_rule_defined(self): + r = self.get('/role_inferences/') + rules = r.result['role_inferences'] + self.assertEqual(self.prior['id'], rules[0]['prior_role']['id']) + self.assertEqual(self.implied1['id'], rules[0]['implies'][0]['id']) + self.assertEqual(self.implied1['name'], rules[0]['implies'][0]['name']) + self.assertEqual(1, len(rules[0]['implies'])) + + def test_list_all_rules(self): + self._setup_prior_two_implied() + self._assert_two_rules_defined() + + self._delete_implied_role(self.prior, self.implied2) + self._assert_one_rule_defined() + + def test_CRD_implied_roles(self): + + self._setup_prior_two_implied() + self._assert_two_roles_implied() + + self._delete_implied_role(self.prior, self.implied2) + self._assert_one_role_implied() + + def _create_three_roles(self): + self.role_list = [] + for _ in range(3): + role = unit.new_role_ref() + self.role_api.create_role(role['id'], role) + self.role_list.append(role) + + def _create_test_domain_user_project(self): + domain = unit.new_domain_ref() + self.resource_api.create_domain(domain['id'], domain) + user = unit.create_user(self.identity_api, domain_id=domain['id']) + project = unit.new_project_ref(domain_id=domain['id']) + self.resource_api.create_project(project['id'], project) + return domain, user, project + + def _assign_top_role_to_user_on_project(self, user, project): + self.assignment_api.add_role_to_user_and_project( + user['id'], project['id'], self.role_list[0]['id']) + + def _build_effective_role_assignments_url(self, user): + return '/role_assignments?effective&user.id=%(user_id)s' % { + 'user_id': user['id']} + + def _assert_all_roles_in_assignment(self, response, user): + # Now use the list role assignments api to check that all three roles + # appear in the collection + self.assertValidRoleAssignmentListResponse( + response, + expected_length=len(self.role_list), + resource_url=self._build_effective_role_assignments_url(user)) + + def _assert_initial_assignment_in_effective(self, response, user, project): + # The initial assignment should be there (the link url will be + # generated and checked automatically since it matches the assignment) + entity = self.build_role_assignment_entity( + project_id=project['id'], + user_id=user['id'], role_id=self.role_list[0]['id']) + self.assertRoleAssignmentInListResponse(response, entity) + + def _assert_effective_role_for_implied_has_prior_in_links( + self, response, user, project, prior_index, implied_index): + # An effective role for an implied role will have the prior role + # assignment in the links + prior_link = '/prior_roles/%(prior)s/implies/%(implied)s' % { + 'prior': self.role_list[prior_index]['id'], + 'implied': self.role_list[implied_index]['id']} + link = self.build_role_assignment_link( + project_id=project['id'], user_id=user['id'], + role_id=self.role_list[prior_index]['id']) + entity = self.build_role_assignment_entity( + link=link, project_id=project['id'], + user_id=user['id'], role_id=self.role_list[implied_index]['id'], + prior_link=prior_link) + self.assertRoleAssignmentInListResponse(response, entity) + + def test_list_role_assignments_with_implied_roles(self): + """Call ``GET /role_assignments`` with implied role grant. + + Test Plan: + + - Create a domain with a user and a project + - Create 3 roles + - Role 0 implies role 1 and role 1 implies role 2 + - Assign the top role to the project + - Issue the URL to check effective roles on project - this + should return all 3 roles. + - Check the links of the 3 roles indicate the prior role where + appropriate + + """ + (domain, user, project) = self._create_test_domain_user_project() + self._create_three_roles() + self._create_implied_role(self.role_list[0], self.role_list[1]) + self._create_implied_role(self.role_list[1], self.role_list[2]) + self._assign_top_role_to_user_on_project(user, project) + + response = self.get(self._build_effective_role_assignments_url(user)) + r = response + + self._assert_all_roles_in_assignment(r, user) + self._assert_initial_assignment_in_effective(response, user, project) + self._assert_effective_role_for_implied_has_prior_in_links( + response, user, project, 0, 1) + self._assert_effective_role_for_implied_has_prior_in_links( + response, user, project, 1, 2) + + def _create_named_role(self, name): + role = unit.new_role_ref() + role['name'] = name + self.role_api.create_role(role['id'], role) + return role + + def test_root_role_as_implied_role_forbidden(self): + """Test root role is forbidden to be set as an implied role. + + Create 2 roles that are prohibited from being an implied role. + Create 1 additional role which should be accepted as an implied + role. Assure the prohibited role names cannot be set as an implied + role. Assure the accepted role name which is not a member of the + prohibited implied role list can be successfully set an implied + role. + """ + prohibited_name1 = 'root1' + prohibited_name2 = 'root2' + accepted_name1 = 'implied1' + + prohibited_names = [prohibited_name1, prohibited_name2] + self.config_fixture.config(group='assignment', + prohibited_implied_role=prohibited_names) + + prior_role = self._create_role() + + prohibited_role1 = self._create_named_role(prohibited_name1) + url = '/roles/{prior_role_id}/implies/{implied_role_id}'.format( + prior_role_id=prior_role['id'], + implied_role_id=prohibited_role1['id']) + self.put(url, expected_status=http_client.FORBIDDEN) + + prohibited_role2 = self._create_named_role(prohibited_name2) + url = '/roles/{prior_role_id}/implies/{implied_role_id}'.format( + prior_role_id=prior_role['id'], + implied_role_id=prohibited_role2['id']) + self.put(url, expected_status=http_client.FORBIDDEN) + + accepted_role1 = self._create_named_role(accepted_name1) + url = '/roles/{prior_role_id}/implies/{implied_role_id}'.format( + prior_role_id=prior_role['id'], + implied_role_id=accepted_role1['id']) + self.put(url, expected_status=http_client.CREATED) + + def test_trusts_from_implied_role(self): + self._create_three_roles() + self._create_implied_role(self.role_list[0], self.role_list[1]) + self._create_implied_role(self.role_list[1], self.role_list[2]) + self._assign_top_role_to_user_on_project(self.user, self.project) + + # Create a trustee and assign the prior role to her + trustee = unit.create_user(self.identity_api, domain_id=self.domain_id) + ref = unit.new_trust_ref( + trustor_user_id=self.user['id'], + trustee_user_id=trustee['id'], + project_id=self.project['id'], + role_ids=[self.role_list[0]['id']]) + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + trust = r.result['trust'] + + # Only the role that was specified is in the trust, NOT implied roles + self.assertEqual(self.role_list[0]['id'], trust['roles'][0]['id']) + self.assertThat(trust['roles'], matchers.HasLength(1)) + + # Authenticate as the trustee + auth_data = self.build_authentication_request( + user_id=trustee['id'], + password=trustee['password'], + trust_id=trust['id']) + r = self.v3_create_token(auth_data) + token = r.result['token'] + self.assertThat(token['roles'], + matchers.HasLength(len(self.role_list))) + for role in token['roles']: + self.assertIn(role, self.role_list) + for role in self.role_list: + self.assertIn(role, token['roles']) + + def test_trusts_from_domain_specific_implied_role(self): + self._create_three_roles() + # Overwrite the first role with a domain specific role + role = unit.new_role_ref(domain_id=self.domain_id) + self.role_list[0] = self.role_api.create_role(role['id'], role) + self._create_implied_role(self.role_list[0], self.role_list[1]) + self._create_implied_role(self.role_list[1], self.role_list[2]) + self._assign_top_role_to_user_on_project(self.user, self.project) + + # Create a trustee and assign the prior role to her + trustee = unit.create_user(self.identity_api, domain_id=self.domain_id) + ref = unit.new_trust_ref( + trustor_user_id=self.user['id'], + trustee_user_id=trustee['id'], + project_id=self.project['id'], + role_ids=[self.role_list[0]['id']]) + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + trust = r.result['trust'] + + # Only the role that was specified is in the trust, NOT implied roles + self.assertEqual(self.role_list[0]['id'], trust['roles'][0]['id']) + self.assertThat(trust['roles'], matchers.HasLength(1)) + + # Authenticate as the trustee + auth_data = self.build_authentication_request( + user_id=trustee['id'], + password=trustee['password'], + trust_id=trust['id']) + r = self.v3_create_token(auth_data) + token = r.result['token'] + + # The token should have the roles implies by the domain specific role, + # but not the domain specific role itself. + self.assertThat(token['roles'], + matchers.HasLength(len(self.role_list) - 1)) + for role in token['roles']: + self.assertIn(role, self.role_list) + for role in [self.role_list[1], self.role_list[2]]: + self.assertIn(role, token['roles']) + self.assertNotIn(self.role_list[0], token['roles']) + + +class DomainSpecificRoleTests(test_v3.RestfulTestCase, unit.TestCase): + def setUp(self): + def create_role(domain_id=None): + """Call ``POST /roles``.""" + ref = unit.new_role_ref(domain_id=domain_id) + r = self.post( + '/roles', + body={'role': ref}) + return self.assertValidRoleResponse(r, ref) + + super(DomainSpecificRoleTests, self).setUp() + self.domainA = unit.new_domain_ref() + self.resource_api.create_domain(self.domainA['id'], self.domainA) + self.domainB = unit.new_domain_ref() + self.resource_api.create_domain(self.domainB['id'], self.domainB) + + self.global_role1 = create_role() + self.global_role2 = create_role() + # Since there maybe other global roles already created, let's count + # them, so we can ensure we can check subsequent list responses + # are correct + r = self.get('/roles') + self.existing_global_roles = len(r.result['roles']) + + # And now create some domain specific roles + self.domainA_role1 = create_role(domain_id=self.domainA['id']) + self.domainA_role2 = create_role(domain_id=self.domainA['id']) + self.domainB_role = create_role(domain_id=self.domainB['id']) + + def test_get_and_list_domain_specific_roles(self): + # Check we can get a domain specific role + r = self.get('/roles/%s' % self.domainA_role1['id']) + self.assertValidRoleResponse(r, self.domainA_role1) + + # If we list without specifying a domain, we should only get global + # roles back. + r = self.get('/roles') + self.assertValidRoleListResponse( + r, expected_length=self.existing_global_roles) + self.assertRoleInListResponse(r, self.global_role1) + self.assertRoleInListResponse(r, self.global_role2) + self.assertRoleNotInListResponse(r, self.domainA_role1) + self.assertRoleNotInListResponse(r, self.domainA_role2) + self.assertRoleNotInListResponse(r, self.domainB_role) + + # Now list those in domainA, making sure that's all we get back + r = self.get('/roles?domain_id=%s' % self.domainA['id']) + self.assertValidRoleListResponse(r, expected_length=2) + self.assertRoleInListResponse(r, self.domainA_role1) + self.assertRoleInListResponse(r, self.domainA_role2) + + def test_update_domain_specific_roles(self): + self.domainA_role1['name'] = uuid.uuid4().hex + self.patch('/roles/%(role_id)s' % { + 'role_id': self.domainA_role1['id']}, + body={'role': self.domainA_role1}) + r = self.get('/roles/%s' % self.domainA_role1['id']) + self.assertValidRoleResponse(r, self.domainA_role1) + + def test_delete_domain_specific_roles(self): + # Check delete only removes that one domain role + self.delete('/roles/%(role_id)s' % { + 'role_id': self.domainA_role1['id']}) + + self.get('/roles/%s' % self.domainA_role1['id'], + expected_status=http_client.NOT_FOUND) + # Now re-list those in domainA, making sure there's only one left + r = self.get('/roles?domain_id=%s' % self.domainA['id']) + self.assertValidRoleListResponse(r, expected_length=1) + self.assertRoleInListResponse(r, self.domainA_role2) + + +class ListUserProjectsTestCase(test_v3.RestfulTestCase): + """Tests for /users//projects""" + + def load_sample_data(self): + # do not load base class's data, keep it focused on the tests + + self.auths = [] + self.domains = [] + self.projects = [] + self.roles = [] + self.users = [] + + # Create 3 sets of domain, roles, projects, and users to demonstrate + # the right user's data is loaded and only projects they can access + # are returned. + + for _ in range(3): + domain = unit.new_domain_ref() + self.resource_api.create_domain(domain['id'], domain) + + user = unit.create_user(self.identity_api, domain_id=domain['id']) + + role = unit.new_role_ref() + self.role_api.create_role(role['id'], role) + + self.assignment_api.create_grant(role['id'], + user_id=user['id'], + domain_id=domain['id']) + + project = unit.new_project_ref(domain_id=domain['id']) + self.resource_api.create_project(project['id'], project) + + self.assignment_api.create_grant(role['id'], + user_id=user['id'], + project_id=project['id']) + + auth = self.build_authentication_request( + user_id=user['id'], + password=user['password'], + domain_id=domain['id']) + + self.auths.append(auth) + self.domains.append(domain) + self.projects.append(project) + self.roles.append(role) + self.users.append(user) + + def test_list_all(self): + for i in range(len(self.users)): + user = self.users[i] + auth = self.auths[i] + + url = '/users/%s/projects' % user['id'] + result = self.get(url, auth=auth) + projects_result = result.json['projects'] + self.assertEqual(1, len(projects_result)) + self.assertEqual(self.projects[i]['id'], projects_result[0]['id']) + + def test_list_enabled(self): + for i in range(len(self.users)): + user = self.users[i] + auth = self.auths[i] + + # There are no disabled projects + url = '/users/%s/projects?enabled=True' % user['id'] + result = self.get(url, auth=auth) + projects_result = result.json['projects'] + self.assertEqual(1, len(projects_result)) + self.assertEqual(self.projects[i]['id'], projects_result[0]['id']) + + def test_list_disabled(self): + for i in range(len(self.users)): + user = self.users[i] + auth = self.auths[i] + project = self.projects[i] + + # There are no disabled projects + url = '/users/%s/projects?enabled=False' % user['id'] + result = self.get(url, auth=auth) + self.assertEqual(0, len(result.json['projects'])) + + # disable this one and check again + project['enabled'] = False + self.resource_api.update_project(project['id'], project) + result = self.get(url, auth=auth) + projects_result = result.json['projects'] + self.assertEqual(1, len(projects_result)) + self.assertEqual(self.projects[i]['id'], projects_result[0]['id']) + + def test_list_by_domain_id(self): + for i in range(len(self.users)): + user = self.users[i] + domain = self.domains[i] + auth = self.auths[i] + + # Try looking for projects with a non-existent domain_id + url = '/users/%s/projects?domain_id=%s' % (user['id'], + uuid.uuid4().hex) + result = self.get(url, auth=auth) + self.assertEqual(0, len(result.json['projects'])) + + # Now try a valid one + url = '/users/%s/projects?domain_id=%s' % (user['id'], + domain['id']) + result = self.get(url, auth=auth) + projects_result = result.json['projects'] + self.assertEqual(1, len(projects_result)) + self.assertEqual(self.projects[i]['id'], projects_result[0]['id']) diff --git a/keystone-moon/keystone/tests/unit/test_v3_auth.py b/keystone-moon/keystone/tests/unit/test_v3_auth.py index d53a85df..698feeb8 100644 --- a/keystone-moon/keystone/tests/unit/test_v3_auth.py +++ b/keystone-moon/keystone/tests/unit/test_v3_auth.py @@ -14,6 +14,7 @@ import copy import datetime +import itertools import json import operator import uuid @@ -21,6 +22,8 @@ import uuid from keystoneclient.common import cms import mock from oslo_config import cfg +from oslo_log import versionutils +from oslo_utils import fixture from oslo_utils import timeutils from six.moves import http_client from six.moves import range @@ -28,9 +31,12 @@ from testtools import matchers from testtools import testcase from keystone import auth +from keystone.auth.plugins import totp from keystone.common import utils +from keystone.contrib.revoke import routers from keystone import exception from keystone.policy.backends import rules +from keystone.tests.common import auth as common_auth from keystone.tests import unit from keystone.tests.unit import ksfixtures from keystone.tests.unit import test_v3 @@ -38,7 +44,7 @@ from keystone.tests.unit import test_v3 CONF = cfg.CONF -class TestAuthInfo(test_v3.AuthTestMixin, testcase.TestCase): +class TestAuthInfo(common_auth.AuthTestMixin, testcase.TestCase): def setUp(self): super(TestAuthInfo, self).setUp() auth.controllers.load_auth_methods() @@ -121,7 +127,7 @@ class TokenAPITests(object): # resolved in Python for multiple inheritance means that a setUp in this # would get skipped by the testrunner. def doSetUp(self): - r = self.v3_authenticate_token(self.build_authentication_request( + r = self.v3_create_token(self.build_authentication_request( username=self.user['name'], user_domain_id=self.domain_id, password=self.user['password'])) @@ -129,146 +135,473 @@ class TokenAPITests(object): self.v3_token = r.headers.get('X-Subject-Token') self.headers = {'X-Subject-Token': r.headers.get('X-Subject-Token')} - def test_default_fixture_scope_token(self): - self.assertIsNotNone(self.get_scoped_token()) + def _make_auth_request(self, auth_data): + resp = self.post('/auth/tokens', body=auth_data) + token = resp.headers.get('X-Subject-Token') + return token - def test_v3_v2_intermix_non_default_domain_failed(self): - v3_token = self.get_requested_token(self.build_authentication_request( + def _get_unscoped_token(self): + auth_data = self.build_authentication_request( user_id=self.user['id'], - password=self.user['password'])) - - # now validate the v3 token with v2 API - self.admin_request( - path='/v2.0/tokens/%s' % v3_token, - token=CONF.admin_token, - method='GET', - expected_status=http_client.UNAUTHORIZED) + password=self.user['password']) + return self._make_auth_request(auth_data) - def test_v3_v2_intermix_new_default_domain(self): - # If the default_domain_id config option is changed, then should be - # able to validate a v3 token with user in the new domain. + def _get_domain_scoped_token(self): + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + domain_id=self.domain_id) + return self._make_auth_request(auth_data) - # 1) Create a new domain for the user. - new_domain = { - 'description': uuid.uuid4().hex, - 'enabled': True, - 'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex, - } - self.resource_api.create_domain(new_domain['id'], new_domain) + def _get_project_scoped_token(self): + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project_id) + return self._make_auth_request(auth_data) - # 2) Create user in new domain. - new_user_password = uuid.uuid4().hex - new_user = { - 'name': uuid.uuid4().hex, - 'domain_id': new_domain['id'], - 'password': new_user_password, - 'email': uuid.uuid4().hex, - } - new_user = self.identity_api.create_user(new_user) + def _get_trust_scoped_token(self, trustee_user, trust): + auth_data = self.build_authentication_request( + user_id=trustee_user['id'], + password=trustee_user['password'], + trust_id=trust['id']) + return self._make_auth_request(auth_data) - # 3) Update the default_domain_id config option to the new domain - self.config_fixture.config( - group='identity', - default_domain_id=new_domain['id']) + def _create_trust(self, impersonation=False): + # Create a trustee user + trustee_user = unit.create_user(self.identity_api, + domain_id=self.domain_id) + ref = unit.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=trustee_user['id'], + project_id=self.project_id, + impersonation=impersonation, + role_ids=[self.role_id]) - # 4) Get a token using v3 API. - v3_token = self.get_requested_token(self.build_authentication_request( - user_id=new_user['id'], - password=new_user_password)) + # Create a trust + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + trust = self.assertValidTrustResponse(r) + return (trustee_user, trust) - # 5) Validate token using v2 API. - self.admin_request( - path='/v2.0/tokens/%s' % v3_token, - token=CONF.admin_token, - method='GET') + def _validate_token(self, token, expected_status=http_client.OK): + return self.get( + '/auth/tokens', + headers={'X-Subject-Token': token}, + expected_status=expected_status) - def test_v3_v2_intermix_domain_scoped_token_failed(self): - # grant the domain role to user - self.put( - path='/domains/%s/users/%s/roles/%s' % ( - self.domain['id'], self.user['id'], self.role['id'])) + def _revoke_token(self, token, expected_status=http_client.NO_CONTENT): + return self.delete( + '/auth/tokens', + headers={'x-subject-token': token}, + expected_status=expected_status) - # generate a domain-scoped v3 token - v3_token = self.get_requested_token(self.build_authentication_request( - user_id=self.user['id'], - password=self.user['password'], - domain_id=self.domain['id'])) + def _set_user_enabled(self, user, enabled=True): + user['enabled'] = enabled + self.identity_api.update_user(user['id'], user) - # domain-scoped tokens are not supported by v2 - self.admin_request( - method='GET', - path='/v2.0/tokens/%s' % v3_token, - token=CONF.admin_token, - expected_status=http_client.UNAUTHORIZED) + def test_validate_unscoped_token(self): + unscoped_token = self._get_unscoped_token() + self._validate_token(unscoped_token) - def test_v3_v2_intermix_non_default_project_failed(self): - # self.project is in a non-default domain - v3_token = self.get_requested_token(self.build_authentication_request( - user_id=self.default_domain_user['id'], - password=self.default_domain_user['password'], - project_id=self.project['id'])) + def test_revoke_unscoped_token(self): + unscoped_token = self._get_unscoped_token() + self._validate_token(unscoped_token) + self._revoke_token(unscoped_token) + self._validate_token(unscoped_token, + expected_status=http_client.NOT_FOUND) - # v2 cannot reference projects outside the default domain - self.admin_request( - method='GET', - path='/v2.0/tokens/%s' % v3_token, - token=CONF.admin_token, - expected_status=http_client.UNAUTHORIZED) + def test_unscoped_token_is_invalid_after_disabling_user(self): + unscoped_token = self._get_unscoped_token() + # Make sure the token is valid + self._validate_token(unscoped_token) + # Disable the user + self._set_user_enabled(self.user, enabled=False) + # Ensure validating a token for a disabled user fails + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + unscoped_token) - def test_v3_v2_intermix_non_default_user_failed(self): - self.assignment_api.create_grant( - self.role['id'], - user_id=self.user['id'], - project_id=self.default_domain_project['id']) + def test_unscoped_token_is_invalid_after_enabling_disabled_user(self): + unscoped_token = self._get_unscoped_token() + # Make sure the token is valid + self._validate_token(unscoped_token) + # Disable the user + self._set_user_enabled(self.user, enabled=False) + # Ensure validating a token for a disabled user fails + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + unscoped_token) + # Enable the user + self._set_user_enabled(self.user) + # Ensure validating a token for a re-enabled user fails + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + unscoped_token) - # self.user is in a non-default domain - v3_token = self.get_requested_token(self.build_authentication_request( - user_id=self.user['id'], - password=self.user['password'], - project_id=self.default_domain_project['id'])) + def test_unscoped_token_is_invalid_after_disabling_user_domain(self): + unscoped_token = self._get_unscoped_token() + # Make sure the token is valid + self._validate_token(unscoped_token) + # Disable the user's domain + self.domain['enabled'] = False + self.resource_api.update_domain(self.domain['id'], self.domain) + # Ensure validating a token for a disabled user fails + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + unscoped_token) - # v2 cannot reference projects outside the default domain - self.admin_request( - method='GET', - path='/v2.0/tokens/%s' % v3_token, - token=CONF.admin_token, - expected_status=http_client.UNAUTHORIZED) + def test_unscoped_token_is_invalid_after_changing_user_password(self): + unscoped_token = self._get_unscoped_token() + # Make sure the token is valid + self._validate_token(unscoped_token) + # Change user's password + self.user['password'] = 'Password1' + self.identity_api.update_user(self.user['id'], self.user) + # Ensure updating user's password revokes existing user's tokens + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + unscoped_token) - def test_v3_v2_intermix_domain_scope_failed(self): - self.assignment_api.create_grant( - self.role['id'], - user_id=self.default_domain_user['id'], - domain_id=self.domain['id']) + def test_validate_domain_scoped_token(self): + # Grant user access to domain + self.assignment_api.create_grant(self.role['id'], + user_id=self.user['id'], + domain_id=self.domain['id']) + domain_scoped_token = self._get_domain_scoped_token() + resp = self._validate_token(domain_scoped_token) + resp_json = json.loads(resp.body) + self.assertIsNotNone(resp_json['token']['catalog']) + self.assertIsNotNone(resp_json['token']['roles']) + self.assertIsNotNone(resp_json['token']['domain']) - v3_token = self.get_requested_token(self.build_authentication_request( - user_id=self.default_domain_user['id'], - password=self.default_domain_user['password'], - domain_id=self.domain['id'])) + def test_domain_scoped_token_is_invalid_after_disabling_user(self): + # Grant user access to domain + self.assignment_api.create_grant(self.role['id'], + user_id=self.user['id'], + domain_id=self.domain['id']) + domain_scoped_token = self._get_domain_scoped_token() + # Make sure the token is valid + self._validate_token(domain_scoped_token) + # Disable user + self._set_user_enabled(self.user, enabled=False) + # Ensure validating a token for a disabled user fails + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + domain_scoped_token) - # v2 cannot reference projects outside the default domain - self.admin_request( - path='/v2.0/tokens/%s' % v3_token, - token=CONF.admin_token, - method='GET', - expected_status=http_client.UNAUTHORIZED) + def test_domain_scoped_token_is_invalid_after_deleting_grant(self): + # Grant user access to domain + self.assignment_api.create_grant(self.role['id'], + user_id=self.user['id'], + domain_id=self.domain['id']) + domain_scoped_token = self._get_domain_scoped_token() + # Make sure the token is valid + self._validate_token(domain_scoped_token) + # Delete access to domain + self.assignment_api.delete_grant(self.role['id'], + user_id=self.user['id'], + domain_id=self.domain['id']) + # Ensure validating a token for a disabled user fails + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + domain_scoped_token) - def test_v3_v2_unscoped_token_intermix(self): - r = self.v3_authenticate_token(self.build_authentication_request( - user_id=self.default_domain_user['id'], - password=self.default_domain_user['password'])) - self.assertValidUnscopedTokenResponse(r) - v3_token_data = r.result - v3_token = r.headers.get('X-Subject-Token') + def test_domain_scoped_token_invalid_after_disabling_domain(self): + # Grant user access to domain + self.assignment_api.create_grant(self.role['id'], + user_id=self.user['id'], + domain_id=self.domain['id']) + domain_scoped_token = self._get_domain_scoped_token() + # Make sure the token is valid + self._validate_token(domain_scoped_token) + # Disable domain + self.domain['enabled'] = False + self.resource_api.update_domain(self.domain['id'], self.domain) + # Ensure validating a token for a disabled domain fails + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + domain_scoped_token) - # now validate the v3 token with v2 API - r = self.admin_request( - path='/v2.0/tokens/%s' % v3_token, - token=CONF.admin_token, - method='GET') - v2_token_data = r.result + def test_v2_validate_domain_scoped_token_returns_unauthorized(self): + # Test that validating a domain scoped token in v2.0 returns + # unauthorized. + # Grant user access to domain + self.assignment_api.create_grant(self.role['id'], + user_id=self.user['id'], + domain_id=self.domain['id']) - self.assertEqual(v2_token_data['access']['user']['id'], + scoped_token = self._get_domain_scoped_token() + self.assertRaises(exception.Unauthorized, + self.token_provider_api.validate_v2_token, + scoped_token) + + def test_validate_project_scoped_token(self): + project_scoped_token = self._get_project_scoped_token() + self._validate_token(project_scoped_token) + + def test_revoke_project_scoped_token(self): + project_scoped_token = self._get_project_scoped_token() + self._validate_token(project_scoped_token) + self._revoke_token(project_scoped_token) + self._validate_token(project_scoped_token, + expected_status=http_client.NOT_FOUND) + + def test_project_scoped_token_is_invalid_after_disabling_user(self): + project_scoped_token = self._get_project_scoped_token() + # Make sure the token is valid + self._validate_token(project_scoped_token) + # Disable the user + self._set_user_enabled(self.user, enabled=False) + # Ensure validating a token for a disabled user fails + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + project_scoped_token) + + def test_project_scoped_token_invalid_after_changing_user_password(self): + project_scoped_token = self._get_project_scoped_token() + # Make sure the token is valid + self._validate_token(project_scoped_token) + # Update user's password + self.user['password'] = 'Password1' + self.identity_api.update_user(self.user['id'], self.user) + # Ensure updating user's password revokes existing tokens + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + project_scoped_token) + + def test_project_scoped_token_invalid_after_disabling_project(self): + project_scoped_token = self._get_project_scoped_token() + # Make sure the token is valid + self._validate_token(project_scoped_token) + # Disable project + self.project['enabled'] = False + self.resource_api.update_project(self.project['id'], self.project) + # Ensure validating a token for a disabled project fails + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + project_scoped_token) + + def test_rescope_unscoped_token_with_trust(self): + trustee_user, trust = self._create_trust() + self._get_trust_scoped_token(trustee_user, trust) + + def test_validate_a_trust_scoped_token(self): + trustee_user, trust = self._create_trust() + trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) + # Validate a trust scoped token + self._validate_token(trust_scoped_token) + + def test_validate_a_trust_scoped_token_impersonated(self): + trustee_user, trust = self._create_trust(impersonation=True) + trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) + # Validate a trust scoped token + self._validate_token(trust_scoped_token) + + def test_revoke_trust_scoped_token(self): + trustee_user, trust = self._create_trust() + trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) + # Validate a trust scoped token + self._validate_token(trust_scoped_token) + self._revoke_token(trust_scoped_token) + self._validate_token(trust_scoped_token, + expected_status=http_client.NOT_FOUND) + + def test_trust_scoped_token_is_invalid_after_disabling_trustee(self): + trustee_user, trust = self._create_trust() + trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) + # Validate a trust scoped token + self._validate_token(trust_scoped_token) + + # Disable trustee + trustee_update_ref = dict(enabled=False) + self.identity_api.update_user(trustee_user['id'], trustee_update_ref) + # Ensure validating a token for a disabled user fails + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + trust_scoped_token) + + def test_trust_scoped_token_invalid_after_changing_trustee_password(self): + trustee_user, trust = self._create_trust() + trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) + # Validate a trust scoped token + self._validate_token(trust_scoped_token) + # Change trustee's password + trustee_update_ref = dict(password='Password1') + self.identity_api.update_user(trustee_user['id'], trustee_update_ref) + # Ensure updating trustee's password revokes existing tokens + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + trust_scoped_token) + + def test_trust_scoped_token_is_invalid_after_disabling_trustor(self): + trustee_user, trust = self._create_trust() + trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) + # Validate a trust scoped token + self._validate_token(trust_scoped_token) + + # Disable the trustor + trustor_update_ref = dict(enabled=False) + self.identity_api.update_user(self.user['id'], trustor_update_ref) + # Ensure validating a token for a disabled user fails + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + trust_scoped_token) + + def test_trust_scoped_token_invalid_after_changing_trustor_password(self): + trustee_user, trust = self._create_trust() + trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) + # Validate a trust scoped token + self._validate_token(trust_scoped_token) + + # Change trustor's password + trustor_update_ref = dict(password='Password1') + self.identity_api.update_user(self.user['id'], trustor_update_ref) + # Ensure updating trustor's password revokes existing user's tokens + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + trust_scoped_token) + + def test_trust_scoped_token_invalid_after_disabled_trustor_domain(self): + trustee_user, trust = self._create_trust() + trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) + # Validate a trust scoped token + self._validate_token(trust_scoped_token) + + # Disable trustor's domain + self.domain['enabled'] = False + self.resource_api.update_domain(self.domain['id'], self.domain) + + trustor_update_ref = dict(password='Password1') + self.identity_api.update_user(self.user['id'], trustor_update_ref) + # Ensure updating trustor's password revokes existing user's tokens + self.assertRaises(exception.TokenNotFound, + self.token_provider_api.validate_token, + trust_scoped_token) + + def test_v2_validate_trust_scoped_token(self): + # Test that validating an trust scoped token in v2.0 returns + # unauthorized. + trustee_user, trust = self._create_trust() + trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) + self.assertRaises(exception.Unauthorized, + self.token_provider_api.validate_v2_token, + trust_scoped_token) + + def test_default_fixture_scope_token(self): + self.assertIsNotNone(self.get_scoped_token()) + + def test_v3_v2_intermix_new_default_domain(self): + # If the default_domain_id config option is changed, then should be + # able to validate a v3 token with user in the new domain. + + # 1) Create a new domain for the user. + new_domain = unit.new_domain_ref() + self.resource_api.create_domain(new_domain['id'], new_domain) + + # 2) Create user in new domain. + new_user = unit.create_user(self.identity_api, + domain_id=new_domain['id']) + + # 3) Update the default_domain_id config option to the new domain + self.config_fixture.config( + group='identity', + default_domain_id=new_domain['id']) + + # 4) Get a token using v3 API. + v3_token = self.get_requested_token(self.build_authentication_request( + user_id=new_user['id'], + password=new_user['password'])) + + # 5) Validate token using v2 API. + self.admin_request( + path='/v2.0/tokens/%s' % v3_token, + token=self.get_admin_token(), + method='GET') + + def test_v3_v2_intermix_domain_scoped_token_failed(self): + # grant the domain role to user + self.put( + path='/domains/%s/users/%s/roles/%s' % ( + self.domain['id'], self.user['id'], self.role['id'])) + + # generate a domain-scoped v3 token + v3_token = self.get_requested_token(self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + domain_id=self.domain['id'])) + + # domain-scoped tokens are not supported by v2 + self.admin_request( + method='GET', + path='/v2.0/tokens/%s' % v3_token, + token=self.get_admin_token(), + expected_status=http_client.UNAUTHORIZED) + + def test_v3_v2_intermix_non_default_project_succeed(self): + # self.project is in a non-default domain + v3_token = self.get_requested_token(self.build_authentication_request( + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password'], + project_id=self.project['id'])) + + # v2 cannot reference projects outside the default domain + self.admin_request( + method='GET', + path='/v2.0/tokens/%s' % v3_token, + token=self.get_admin_token()) + + def test_v3_v2_intermix_non_default_user_succeed(self): + self.assignment_api.create_grant( + self.role['id'], + user_id=self.user['id'], + project_id=self.default_domain_project['id']) + + # self.user is in a non-default domain + v3_token = self.get_requested_token(self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.default_domain_project['id'])) + + # v2 cannot reference projects outside the default domain + self.admin_request( + method='GET', + path='/v2.0/tokens/%s' % v3_token, + token=self.get_admin_token()) + + def test_v3_v2_intermix_domain_scope_failed(self): + self.assignment_api.create_grant( + self.role['id'], + user_id=self.default_domain_user['id'], + domain_id=self.domain['id']) + + v3_token = self.get_requested_token(self.build_authentication_request( + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password'], + domain_id=self.domain['id'])) + + # v2 cannot reference projects outside the default domain + self.admin_request( + path='/v2.0/tokens/%s' % v3_token, + token=self.get_admin_token(), + method='GET', + expected_status=http_client.UNAUTHORIZED) + + def test_v3_v2_unscoped_token_intermix(self): + r = self.v3_create_token(self.build_authentication_request( + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password'])) + self.assertValidUnscopedTokenResponse(r) + v3_token_data = r.result + v3_token = r.headers.get('X-Subject-Token') + + # now validate the v3 token with v2 API + r = self.admin_request( + path='/v2.0/tokens/%s' % v3_token, + token=self.get_admin_token(), + method='GET') + v2_token_data = r.result + + self.assertEqual(v2_token_data['access']['user']['id'], v3_token_data['token']['user']['id']) # v2 token time has not fraction of second precision so # just need to make sure the non fraction part agrees @@ -278,7 +611,7 @@ class TokenAPITests(object): def test_v3_v2_token_intermix(self): # FIXME(gyee): PKI tokens are not interchangeable because token # data is baked into the token itself. - r = self.v3_authenticate_token(self.build_authentication_request( + r = self.v3_create_token(self.build_authentication_request( user_id=self.default_domain_user['id'], password=self.default_domain_user['password'], project_id=self.default_domain_project['id'])) @@ -290,7 +623,7 @@ class TokenAPITests(object): r = self.admin_request( method='GET', path='/v2.0/tokens/%s' % v3_token, - token=CONF.admin_token) + token=self.get_admin_token()) v2_token_data = r.result self.assertEqual(v2_token_data['access']['user']['id'], @@ -318,9 +651,7 @@ class TokenAPITests(object): v2_token = v2_token_data['access']['token']['id'] r = self.get('/auth/tokens', headers={'X-Subject-Token': v2_token}) - # FIXME(dolph): Due to bug 1476329, v2 tokens validated on v3 are - # missing timezones, so they will not pass this assertion. - # self.assertValidUnscopedTokenResponse(r) + self.assertValidUnscopedTokenResponse(r) v3_token_data = r.result self.assertEqual(v2_token_data['access']['user']['id'], @@ -347,9 +678,7 @@ class TokenAPITests(object): v2_token = v2_token_data['access']['token']['id'] r = self.get('/auth/tokens', headers={'X-Subject-Token': v2_token}) - # FIXME(dolph): Due to bug 1476329, v2 tokens validated on v3 are - # missing timezones, so they will not pass this assertion. - # self.assertValidProjectScopedTokenResponse(r) + self.assertValidProjectScopedTokenResponse(r) v3_token_data = r.result self.assertEqual(v2_token_data['access']['user']['id'], @@ -384,9 +713,8 @@ class TokenAPITests(object): v2_token = r.result['access']['token']['id'] # Delete the v2 token using v3. - resp = self.delete( + self.delete( '/auth/tokens', headers={'X-Subject-Token': v2_token}) - self.assertEqual(resp.status_code, 204) # Attempting to use the deleted token on v2 should fail. self.admin_request( @@ -397,7 +725,7 @@ class TokenAPITests(object): expires = self.v3_token_data['token']['expires_at'] # rescope the token - r = self.v3_authenticate_token(self.build_authentication_request( + r = self.v3_create_token(self.build_authentication_request( token=self.v3_token, project_id=self.project_id)) self.assertValidProjectScopedTokenResponse(r) @@ -406,12 +734,24 @@ class TokenAPITests(object): self.assertEqual(expires, r.result['token']['expires_at']) def test_check_token(self): - self.head('/auth/tokens', headers=self.headers, expected_status=200) + self.head('/auth/tokens', headers=self.headers, + expected_status=http_client.OK) def test_validate_token(self): r = self.get('/auth/tokens', headers=self.headers) self.assertValidUnscopedTokenResponse(r) + def test_validate_missing_subject_token(self): + self.get('/auth/tokens', + expected_status=http_client.NOT_FOUND) + + def test_validate_missing_auth_token(self): + self.admin_request( + method='GET', + path='/v3/projects', + token=None, + expected_status=http_client.UNAUTHORIZED) + def test_validate_token_nocatalog(self): v3_token = self.get_requested_token(self.build_authentication_request( user_id=self.user['id'], @@ -422,6 +762,399 @@ class TokenAPITests(object): headers={'X-Subject-Token': v3_token}) self.assertValidProjectScopedTokenResponse(r, require_catalog=False) + def test_is_admin_token_by_ids(self): + self.config_fixture.config( + group='resource', + admin_project_domain_name=self.domain['name'], + admin_project_name=self.project['name']) + r = self.v3_create_token(self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project['id'])) + self.assertValidProjectScopedTokenResponse(r, is_admin_project=True) + v3_token = r.headers.get('X-Subject-Token') + r = self.get('/auth/tokens', headers={'X-Subject-Token': v3_token}) + self.assertValidProjectScopedTokenResponse(r, is_admin_project=True) + + def test_is_admin_token_by_names(self): + self.config_fixture.config( + group='resource', + admin_project_domain_name=self.domain['name'], + admin_project_name=self.project['name']) + r = self.v3_create_token(self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_domain_name=self.domain['name'], + project_name=self.project['name'])) + self.assertValidProjectScopedTokenResponse(r, is_admin_project=True) + v3_token = r.headers.get('X-Subject-Token') + r = self.get('/auth/tokens', headers={'X-Subject-Token': v3_token}) + self.assertValidProjectScopedTokenResponse(r, is_admin_project=True) + + def test_token_for_non_admin_project_is_not_admin(self): + self.config_fixture.config( + group='resource', + admin_project_domain_name=self.domain['name'], + admin_project_name=uuid.uuid4().hex) + r = self.v3_create_token(self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project['id'])) + self.assertValidProjectScopedTokenResponse(r, is_admin_project=False) + v3_token = r.headers.get('X-Subject-Token') + r = self.get('/auth/tokens', headers={'X-Subject-Token': v3_token}) + self.assertValidProjectScopedTokenResponse(r, is_admin_project=False) + + def test_token_for_non_admin_domain_same_project_name_is_not_admin(self): + self.config_fixture.config( + group='resource', + admin_project_domain_name=uuid.uuid4().hex, + admin_project_name=self.project['name']) + r = self.v3_create_token(self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project['id'])) + self.assertValidProjectScopedTokenResponse(r, is_admin_project=False) + v3_token = r.headers.get('X-Subject-Token') + r = self.get('/auth/tokens', headers={'X-Subject-Token': v3_token}) + self.assertValidProjectScopedTokenResponse(r, is_admin_project=False) + + def test_only_admin_project_set_acts_as_non_admin(self): + self.config_fixture.config( + group='resource', + admin_project_name=self.project['name']) + r = self.v3_create_token(self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project['id'])) + self.assertValidProjectScopedTokenResponse(r, is_admin_project=False) + v3_token = r.headers.get('X-Subject-Token') + r = self.get('/auth/tokens', headers={'X-Subject-Token': v3_token}) + self.assertValidProjectScopedTokenResponse(r, is_admin_project=False) + + def _create_role(self, domain_id=None): + """Call ``POST /roles``.""" + ref = unit.new_role_ref(domain_id=domain_id) + r = self.post('/roles', body={'role': ref}) + return self.assertValidRoleResponse(r, ref) + + def _create_implied_role(self, prior_id): + implied = self._create_role() + url = '/roles/%s/implies/%s' % (prior_id, implied['id']) + self.put(url, expected_status=http_client.CREATED) + return implied + + def _delete_implied_role(self, prior_role_id, implied_role_id): + url = '/roles/%s/implies/%s' % (prior_role_id, implied_role_id) + self.delete(url) + + def _get_scoped_token_roles(self, is_domain=False): + if is_domain: + v3_token = self.get_domain_scoped_token() + else: + v3_token = self.get_scoped_token() + + r = self.get('/auth/tokens', headers={'X-Subject-Token': v3_token}) + v3_token_data = r.result + token_roles = v3_token_data['token']['roles'] + return token_roles + + def _create_implied_role_shows_in_v3_token(self, is_domain): + token_roles = self._get_scoped_token_roles(is_domain) + self.assertEqual(1, len(token_roles)) + + prior = token_roles[0]['id'] + implied1 = self._create_implied_role(prior) + + token_roles = self._get_scoped_token_roles(is_domain) + self.assertEqual(2, len(token_roles)) + + implied2 = self._create_implied_role(prior) + token_roles = self._get_scoped_token_roles(is_domain) + self.assertEqual(3, len(token_roles)) + + token_role_ids = [role['id'] for role in token_roles] + self.assertIn(prior, token_role_ids) + self.assertIn(implied1['id'], token_role_ids) + self.assertIn(implied2['id'], token_role_ids) + + def test_create_implied_role_shows_in_v3_project_token(self): + # regardless of the default chosen, this should always + # test with the option set. + self.config_fixture.config(group='token', infer_roles=True) + self._create_implied_role_shows_in_v3_token(False) + + def test_create_implied_role_shows_in_v3_domain_token(self): + self.config_fixture.config(group='token', infer_roles=True) + self.assignment_api.create_grant(self.role['id'], + user_id=self.user['id'], + domain_id=self.domain['id']) + + self._create_implied_role_shows_in_v3_token(True) + + def test_group_assigned_implied_role_shows_in_v3_token(self): + self.config_fixture.config(group='token', infer_roles=True) + is_domain = False + token_roles = self._get_scoped_token_roles(is_domain) + self.assertEqual(1, len(token_roles)) + + new_role = self._create_role() + prior = new_role['id'] + + new_group_ref = unit.new_group_ref(domain_id=self.domain['id']) + new_group = self.identity_api.create_group(new_group_ref) + self.assignment_api.create_grant(prior, + group_id=new_group['id'], + project_id=self.project['id']) + + token_roles = self._get_scoped_token_roles(is_domain) + self.assertEqual(1, len(token_roles)) + + self.identity_api.add_user_to_group(self.user['id'], + new_group['id']) + + token_roles = self._get_scoped_token_roles(is_domain) + self.assertEqual(2, len(token_roles)) + + implied1 = self._create_implied_role(prior) + + token_roles = self._get_scoped_token_roles(is_domain) + self.assertEqual(3, len(token_roles)) + + implied2 = self._create_implied_role(prior) + token_roles = self._get_scoped_token_roles(is_domain) + self.assertEqual(4, len(token_roles)) + + token_role_ids = [role['id'] for role in token_roles] + self.assertIn(prior, token_role_ids) + self.assertIn(implied1['id'], token_role_ids) + self.assertIn(implied2['id'], token_role_ids) + + def test_multiple_implied_roles_show_in_v3_token(self): + self.config_fixture.config(group='token', infer_roles=True) + token_roles = self._get_scoped_token_roles() + self.assertEqual(1, len(token_roles)) + + prior = token_roles[0]['id'] + implied1 = self._create_implied_role(prior) + implied2 = self._create_implied_role(prior) + implied3 = self._create_implied_role(prior) + + token_roles = self._get_scoped_token_roles() + self.assertEqual(4, len(token_roles)) + + token_role_ids = [role['id'] for role in token_roles] + self.assertIn(prior, token_role_ids) + self.assertIn(implied1['id'], token_role_ids) + self.assertIn(implied2['id'], token_role_ids) + self.assertIn(implied3['id'], token_role_ids) + + def test_chained_implied_role_shows_in_v3_token(self): + self.config_fixture.config(group='token', infer_roles=True) + token_roles = self._get_scoped_token_roles() + self.assertEqual(1, len(token_roles)) + + prior = token_roles[0]['id'] + implied1 = self._create_implied_role(prior) + implied2 = self._create_implied_role(implied1['id']) + implied3 = self._create_implied_role(implied2['id']) + + token_roles = self._get_scoped_token_roles() + self.assertEqual(4, len(token_roles)) + + token_role_ids = [role['id'] for role in token_roles] + + self.assertIn(prior, token_role_ids) + self.assertIn(implied1['id'], token_role_ids) + self.assertIn(implied2['id'], token_role_ids) + self.assertIn(implied3['id'], token_role_ids) + + def test_implied_role_disabled_by_config(self): + self.config_fixture.config(group='token', infer_roles=False) + token_roles = self._get_scoped_token_roles() + self.assertEqual(1, len(token_roles)) + + prior = token_roles[0]['id'] + implied1 = self._create_implied_role(prior) + implied2 = self._create_implied_role(implied1['id']) + self._create_implied_role(implied2['id']) + + token_roles = self._get_scoped_token_roles() + self.assertEqual(1, len(token_roles)) + token_role_ids = [role['id'] for role in token_roles] + self.assertIn(prior, token_role_ids) + + def test_delete_implied_role_do_not_show_in_v3_token(self): + self.config_fixture.config(group='token', infer_roles=True) + token_roles = self._get_scoped_token_roles() + prior = token_roles[0]['id'] + implied = self._create_implied_role(prior) + + token_roles = self._get_scoped_token_roles() + self.assertEqual(2, len(token_roles)) + self._delete_implied_role(prior, implied['id']) + + token_roles = self._get_scoped_token_roles() + self.assertEqual(1, len(token_roles)) + + def test_unrelated_implied_roles_do_not_change_v3_token(self): + self.config_fixture.config(group='token', infer_roles=True) + token_roles = self._get_scoped_token_roles() + prior = token_roles[0]['id'] + implied = self._create_implied_role(prior) + + token_roles = self._get_scoped_token_roles() + self.assertEqual(2, len(token_roles)) + + unrelated = self._create_role() + url = '/roles/%s/implies/%s' % (unrelated['id'], implied['id']) + self.put(url, expected_status=http_client.CREATED) + + token_roles = self._get_scoped_token_roles() + self.assertEqual(2, len(token_roles)) + + self._delete_implied_role(unrelated['id'], implied['id']) + token_roles = self._get_scoped_token_roles() + self.assertEqual(2, len(token_roles)) + + def test_domain_scpecific_roles_do_not_show_v3_token(self): + self.config_fixture.config(group='token', infer_roles=True) + initial_token_roles = self._get_scoped_token_roles() + + new_role = self._create_role(domain_id=self.domain_id) + self.assignment_api.create_grant(new_role['id'], + user_id=self.user['id'], + project_id=self.project['id']) + implied = self._create_implied_role(new_role['id']) + + token_roles = self._get_scoped_token_roles() + self.assertEqual(len(initial_token_roles) + 1, len(token_roles)) + + # The implied role from the domain specific role should be in the + # token, but not the domain specific role itself. + token_role_ids = [role['id'] for role in token_roles] + self.assertIn(implied['id'], token_role_ids) + self.assertNotIn(new_role['id'], token_role_ids) + + def test_remove_all_roles_from_scope_result_in_404(self): + # create a new user + new_user = unit.create_user(self.identity_api, + domain_id=self.domain['id']) + + # give the new user a role on a project + path = '/projects/%s/users/%s/roles/%s' % ( + self.project['id'], new_user['id'], self.role['id']) + self.put(path=path) + + # authenticate as the new user and get a project-scoped token + auth_data = self.build_authentication_request( + user_id=new_user['id'], + password=new_user['password'], + project_id=self.project['id']) + subject_token_id = self.v3_create_token(auth_data).headers.get( + 'X-Subject-Token') + + # make sure the project-scoped token is valid + headers = {'X-Subject-Token': subject_token_id} + r = self.get('/auth/tokens', headers=headers) + self.assertValidProjectScopedTokenResponse(r) + + # remove the roles from the user for the given scope + path = '/projects/%s/users/%s/roles/%s' % ( + self.project['id'], new_user['id'], self.role['id']) + self.delete(path=path) + + # token validation should now result in 404 + self.get('/auth/tokens', headers=headers, + expected_status=http_client.NOT_FOUND) + + +class TokenDataTests(object): + """Test the data in specific token types.""" + + def test_unscoped_token_format(self): + # ensure the unscoped token response contains the appropriate data + r = self.get('/auth/tokens', headers=self.headers) + self.assertValidUnscopedTokenResponse(r) + + def test_domain_scoped_token_format(self): + # ensure the domain scoped token response contains the appropriate data + self.assignment_api.create_grant( + self.role['id'], + user_id=self.default_domain_user['id'], + domain_id=self.domain['id']) + + domain_scoped_token = self.get_requested_token( + self.build_authentication_request( + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password'], + domain_id=self.domain['id']) + ) + self.headers['X-Subject-Token'] = domain_scoped_token + r = self.get('/auth/tokens', headers=self.headers) + self.assertValidDomainScopedTokenResponse(r) + + def test_project_scoped_token_format(self): + # ensure project scoped token responses contains the appropriate data + project_scoped_token = self.get_requested_token( + self.build_authentication_request( + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password'], + project_id=self.default_domain_project['id']) + ) + self.headers['X-Subject-Token'] = project_scoped_token + r = self.get('/auth/tokens', headers=self.headers) + self.assertValidProjectScopedTokenResponse(r) + + def test_extra_data_in_unscoped_token_fails_validation(self): + # ensure unscoped token response contains the appropriate data + r = self.get('/auth/tokens', headers=self.headers) + + # populate the response result with some extra data + r.result['token'][u'extra'] = unicode(uuid.uuid4().hex) + self.assertRaises(exception.SchemaValidationError, + self.assertValidUnscopedTokenResponse, + r) + + def test_extra_data_in_domain_scoped_token_fails_validation(self): + # ensure domain scoped token response contains the appropriate data + self.assignment_api.create_grant( + self.role['id'], + user_id=self.default_domain_user['id'], + domain_id=self.domain['id']) + + domain_scoped_token = self.get_requested_token( + self.build_authentication_request( + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password'], + domain_id=self.domain['id']) + ) + self.headers['X-Subject-Token'] = domain_scoped_token + r = self.get('/auth/tokens', headers=self.headers) + + # populate the response result with some extra data + r.result['token'][u'extra'] = unicode(uuid.uuid4().hex) + self.assertRaises(exception.SchemaValidationError, + self.assertValidDomainScopedTokenResponse, + r) + + def test_extra_data_in_project_scoped_token_fails_validation(self): + # ensure project scoped token responses contains the appropriate data + project_scoped_token = self.get_requested_token( + self.build_authentication_request( + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password'], + project_id=self.default_domain_project['id']) + ) + self.headers['X-Subject-Token'] = project_scoped_token + resp = self.get('/auth/tokens', headers=self.headers) + + # populate the response result with some extra data + resp.result['token'][u'extra'] = unicode(uuid.uuid4().hex) + self.assertRaises(exception.SchemaValidationError, + self.assertValidProjectScopedTokenResponse, + resp) + class AllowRescopeScopedTokenDisabledTests(test_v3.RestfulTestCase): def config_overrides(self): @@ -431,7 +1164,7 @@ class AllowRescopeScopedTokenDisabledTests(test_v3.RestfulTestCase): allow_rescope_scoped_token=False) def test_rescoping_v3_to_v3_disabled(self): - self.v3_authenticate_token( + self.v3_create_token( self.build_authentication_request( token=self.get_scoped_token(), project_id=self.project_id), @@ -465,7 +1198,7 @@ class AllowRescopeScopedTokenDisabledTests(test_v3.RestfulTestCase): def test_rescoping_v2_to_v3_disabled(self): token = self._v2_token() - self.v3_authenticate_token( + self.v3_create_token( self.build_authentication_request( token=token['access']['token']['id'], project_id=self.project_id), @@ -481,7 +1214,7 @@ class AllowRescopeScopedTokenDisabledTests(test_v3.RestfulTestCase): def test_rescoped_domain_token_disabled(self): - self.domainA = self.new_domain_ref() + self.domainA = unit.new_domain_ref() self.resource_api.create_domain(self.domainA['id'], self.domainA) self.assignment_api.create_grant(self.role['id'], user_id=self.user['id'], @@ -495,14 +1228,14 @@ class AllowRescopeScopedTokenDisabledTests(test_v3.RestfulTestCase): self.build_authentication_request( token=unscoped_token, domain_id=self.domainA['id'])) - self.v3_authenticate_token( + self.v3_create_token( self.build_authentication_request( token=domain_scoped_token, project_id=self.project_id), expected_status=http_client.FORBIDDEN) -class TestPKITokenAPIs(test_v3.RestfulTestCase, TokenAPITests): +class TestPKITokenAPIs(test_v3.RestfulTestCase, TokenAPITests, TokenDataTests): def config_overrides(self): super(TestPKITokenAPIs, self).config_overrides() self.config_fixture.config(group='token', provider='pki') @@ -518,7 +1251,7 @@ class TestPKITokenAPIs(test_v3.RestfulTestCase, TokenAPITests): auth_data = self.build_authentication_request( user_id=self.user['id'], password=self.user['password']) - resp = self.v3_authenticate_token(auth_data) + resp = self.v3_create_token(auth_data) token_data = resp.result token_id = resp.headers.get('X-Subject-Token') self.assertIn('expires_at', token_data['token']) @@ -542,7 +1275,7 @@ class TestPKITokenAPIs(test_v3.RestfulTestCase, TokenAPITests): user_id=self.default_domain_user['id'], password=self.default_domain_user['password'], project_id=self.default_domain_project['id']) - resp = self.v3_authenticate_token(auth_data) + resp = self.v3_create_token(auth_data) token_data = resp.result token = resp.headers.get('X-Subject-Token') @@ -550,7 +1283,7 @@ class TestPKITokenAPIs(test_v3.RestfulTestCase, TokenAPITests): token = cms.cms_hash_token(token) path = '/v2.0/tokens/%s' % (token) resp = self.admin_request(path=path, - token=CONF.admin_token, + token=self.get_admin_token(), method='GET') v2_token = resp.result self.assertEqual(v2_token['access']['user']['id'], @@ -559,8 +1292,8 @@ class TestPKITokenAPIs(test_v3.RestfulTestCase, TokenAPITests): # just need to make sure the non fraction part agrees self.assertIn(v2_token['access']['token']['expires'][:-1], token_data['token']['expires_at']) - self.assertEqual(v2_token['access']['user']['roles'][0]['id'], - token_data['token']['roles'][0]['id']) + self.assertEqual(v2_token['access']['user']['roles'][0]['name'], + token_data['token']['roles'][0]['name']) class TestPKIZTokenAPIs(TestPKITokenAPIs): @@ -572,7 +1305,8 @@ class TestPKIZTokenAPIs(TestPKITokenAPIs): return cms.pkiz_verify(*args, **kwargs) -class TestUUIDTokenAPIs(test_v3.RestfulTestCase, TokenAPITests): +class TestUUIDTokenAPIs(test_v3.RestfulTestCase, TokenAPITests, + TokenDataTests): def config_overrides(self): super(TestUUIDTokenAPIs, self).config_overrides() self.config_fixture.config(group='token', provider='uuid') @@ -585,14 +1319,15 @@ class TestUUIDTokenAPIs(test_v3.RestfulTestCase, TokenAPITests): auth_data = self.build_authentication_request( user_id=self.user['id'], password=self.user['password']) - resp = self.v3_authenticate_token(auth_data) + resp = self.v3_create_token(auth_data) token_data = resp.result token_id = resp.headers.get('X-Subject-Token') self.assertIn('expires_at', token_data['token']) self.assertFalse(cms.is_asn1_token(token_id)) -class TestFernetTokenAPIs(test_v3.RestfulTestCase, TokenAPITests): +class TestFernetTokenAPIs(test_v3.RestfulTestCase, TokenAPITests, + TokenDataTests): def config_overrides(self): super(TestFernetTokenAPIs, self).config_overrides() self.config_fixture.config(group='token', provider='fernet') @@ -602,6 +1337,34 @@ class TestFernetTokenAPIs(test_v3.RestfulTestCase, TokenAPITests): super(TestFernetTokenAPIs, self).setUp() self.doSetUp() + def _make_auth_request(self, auth_data): + token = super(TestFernetTokenAPIs, self)._make_auth_request(auth_data) + self.assertLess(len(token), 255) + return token + + def test_validate_tampered_unscoped_token_fails(self): + unscoped_token = self._get_unscoped_token() + tampered_token = (unscoped_token[:50] + uuid.uuid4().hex + + unscoped_token[50 + 32:]) + self._validate_token(tampered_token, + expected_status=http_client.NOT_FOUND) + + def test_validate_tampered_project_scoped_token_fails(self): + project_scoped_token = self._get_project_scoped_token() + tampered_token = (project_scoped_token[:50] + uuid.uuid4().hex + + project_scoped_token[50 + 32:]) + self._validate_token(tampered_token, + expected_status=http_client.NOT_FOUND) + + def test_validate_tampered_trust_scoped_token_fails(self): + trustee_user, trust = self._create_trust() + trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) + # Get a trust scoped token + tampered_token = (trust_scoped_token[:50] + uuid.uuid4().hex + + trust_scoped_token[50 + 32:]) + self._validate_token(tampered_token, + expected_status=http_client.NOT_FOUND) + class TestTokenRevokeSelfAndAdmin(test_v3.RestfulTestCase): """Test token revoke using v3 Identity API by token owner and admin.""" @@ -616,29 +1379,22 @@ class TestTokenRevokeSelfAndAdmin(test_v3.RestfulTestCase): """ super(TestTokenRevokeSelfAndAdmin, self).load_sample_data() # DomainA setup - self.domainA = self.new_domain_ref() + self.domainA = unit.new_domain_ref() self.resource_api.create_domain(self.domainA['id'], self.domainA) - self.userAdminA = self.new_user_ref(domain_id=self.domainA['id']) - password = self.userAdminA['password'] - self.userAdminA = self.identity_api.create_user(self.userAdminA) - self.userAdminA['password'] = password + self.userAdminA = unit.create_user(self.identity_api, + domain_id=self.domainA['id']) - self.userNormalA = self.new_user_ref( - domain_id=self.domainA['id']) - password = self.userNormalA['password'] - self.userNormalA = self.identity_api.create_user(self.userNormalA) - self.userNormalA['password'] = password + self.userNormalA = unit.create_user(self.identity_api, + domain_id=self.domainA['id']) self.assignment_api.create_grant(self.role['id'], user_id=self.userAdminA['id'], domain_id=self.domainA['id']) - def config_overrides(self): - super(TestTokenRevokeSelfAndAdmin, self).config_overrides() - self.config_fixture.config( - group='oslo_policy', - policy_file=unit.dirs.etc('policy.v3cloudsample.json')) + def _policy_fixture(self): + return ksfixtures.Policy(unit.dirs.etc('policy.v3cloudsample.json'), + self.config_fixture) def test_user_revokes_own_token(self): user_token = self.get_requested_token( @@ -655,11 +1411,13 @@ class TestTokenRevokeSelfAndAdmin(test_v3.RestfulTestCase): password=self.userAdminA['password'], domain_name=self.domainA['name'])) - self.head('/auth/tokens', headers=headers, expected_status=200, + self.head('/auth/tokens', headers=headers, + expected_status=http_client.OK, token=adminA_token) - self.head('/auth/tokens', headers=headers, expected_status=200, + self.head('/auth/tokens', headers=headers, + expected_status=http_client.OK, token=user_token) - self.delete('/auth/tokens', headers=headers, expected_status=204, + self.delete('/auth/tokens', headers=headers, token=user_token) # invalid X-Auth-Token and invalid X-Subject-Token self.head('/auth/tokens', headers=headers, @@ -693,11 +1451,13 @@ class TestTokenRevokeSelfAndAdmin(test_v3.RestfulTestCase): password=self.userAdminA['password'], domain_name=self.domainA['name'])) - self.head('/auth/tokens', headers=headers, expected_status=200, + self.head('/auth/tokens', headers=headers, + expected_status=http_client.OK, token=adminA_token) - self.head('/auth/tokens', headers=headers, expected_status=200, + self.head('/auth/tokens', headers=headers, + expected_status=http_client.OK, token=user_token) - self.delete('/auth/tokens', headers=headers, expected_status=204, + self.delete('/auth/tokens', headers=headers, token=adminA_token) # invalid X-Auth-Token and invalid X-Subject-Token self.head('/auth/tokens', headers=headers, @@ -714,14 +1474,12 @@ class TestTokenRevokeSelfAndAdmin(test_v3.RestfulTestCase): def test_adminB_fails_revoking_userA_token(self): # DomainB setup - self.domainB = self.new_domain_ref() + self.domainB = unit.new_domain_ref() self.resource_api.create_domain(self.domainB['id'], self.domainB) - self.userAdminB = self.new_user_ref(domain_id=self.domainB['id']) - password = self.userAdminB['password'] - self.userAdminB = self.identity_api.create_user(self.userAdminB) - self.userAdminB['password'] = password + userAdminB = unit.create_user(self.identity_api, + domain_id=self.domainB['id']) self.assignment_api.create_grant(self.role['id'], - user_id=self.userAdminB['id'], + user_id=userAdminB['id'], domain_id=self.domainB['id']) user_token = self.get_requested_token( @@ -733,8 +1491,8 @@ class TestTokenRevokeSelfAndAdmin(test_v3.RestfulTestCase): adminB_token = self.get_requested_token( self.build_authentication_request( - user_id=self.userAdminB['id'], - password=self.userAdminB['password'], + user_id=userAdminB['id'], + password=userAdminB['password'], domain_name=self.domainB['name'])) self.head('/auth/tokens', headers=headers, @@ -750,7 +1508,6 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): def config_overrides(self): super(TestTokenRevokeById, self).config_overrides() - self.config_fixture.config(group='revoke', driver='kvs') self.config_fixture.config( group='token', provider='pki', @@ -782,44 +1539,32 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): super(TestTokenRevokeById, self).setUp() # Start by creating a couple of domains and projects - self.domainA = self.new_domain_ref() + self.domainA = unit.new_domain_ref() self.resource_api.create_domain(self.domainA['id'], self.domainA) - self.domainB = self.new_domain_ref() + self.domainB = unit.new_domain_ref() self.resource_api.create_domain(self.domainB['id'], self.domainB) - self.projectA = self.new_project_ref(domain_id=self.domainA['id']) + self.projectA = unit.new_project_ref(domain_id=self.domainA['id']) self.resource_api.create_project(self.projectA['id'], self.projectA) - self.projectB = self.new_project_ref(domain_id=self.domainA['id']) + self.projectB = unit.new_project_ref(domain_id=self.domainA['id']) self.resource_api.create_project(self.projectB['id'], self.projectB) # Now create some users - self.user1 = self.new_user_ref( - domain_id=self.domainA['id']) - password = self.user1['password'] - self.user1 = self.identity_api.create_user(self.user1) - self.user1['password'] = password - - self.user2 = self.new_user_ref( - domain_id=self.domainB['id']) - password = self.user2['password'] - self.user2 = self.identity_api.create_user(self.user2) - self.user2['password'] = password - - self.user3 = self.new_user_ref( - domain_id=self.domainB['id']) - password = self.user3['password'] - self.user3 = self.identity_api.create_user(self.user3) - self.user3['password'] = password - - self.group1 = self.new_group_ref( - domain_id=self.domainA['id']) + self.user1 = unit.create_user(self.identity_api, + domain_id=self.domainA['id']) + + self.user2 = unit.create_user(self.identity_api, + domain_id=self.domainB['id']) + + self.user3 = unit.create_user(self.identity_api, + domain_id=self.domainB['id']) + + self.group1 = unit.new_group_ref(domain_id=self.domainA['id']) self.group1 = self.identity_api.create_group(self.group1) - self.group2 = self.new_group_ref( - domain_id=self.domainA['id']) + self.group2 = unit.new_group_ref(domain_id=self.domainA['id']) self.group2 = self.identity_api.create_group(self.group2) - self.group3 = self.new_group_ref( - domain_id=self.domainB['id']) + self.group3 = unit.new_group_ref(domain_id=self.domainB['id']) self.group3 = self.identity_api.create_group(self.group3) self.identity_api.add_user_to_group(self.user1['id'], @@ -829,9 +1574,9 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): self.identity_api.add_user_to_group(self.user3['id'], self.group2['id']) - self.role1 = self.new_role_ref() + self.role1 = unit.new_role_ref() self.role_api.create_role(self.role1['id'], self.role1) - self.role2 = self.new_role_ref() + self.role2 = unit.new_role_ref() self.role_api.create_role(self.role2['id'], self.role2) self.assignment_api.create_grant(self.role2['id'], @@ -864,13 +1609,13 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): # confirm both tokens are valid self.head('/auth/tokens', headers={'X-Subject-Token': unscoped_token}, - expected_status=200) + expected_status=http_client.OK) self.head('/auth/tokens', headers={'X-Subject-Token': scoped_token}, - expected_status=200) + expected_status=http_client.OK) # create a new role - role = self.new_role_ref() + role = unit.new_role_ref() self.role_api.create_role(role['id'], role) # assign a new role @@ -883,10 +1628,10 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): # both tokens should remain valid self.head('/auth/tokens', headers={'X-Subject-Token': unscoped_token}, - expected_status=200) + expected_status=http_client.OK) self.head('/auth/tokens', headers={'X-Subject-Token': scoped_token}, - expected_status=200) + expected_status=http_client.OK) def test_deleting_user_grant_revokes_token(self): """Test deleting a user grant revokes token. @@ -906,7 +1651,7 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): # Confirm token is valid self.head('/auth/tokens', headers={'X-Subject-Token': token}, - expected_status=200) + expected_status=http_client.OK) # Delete the grant, which should invalidate the token grant_url = ( '/projects/%(project_id)s/users/%(user_id)s/' @@ -920,22 +1665,14 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): expected_status=http_client.NOT_FOUND) def role_data_fixtures(self): - self.projectC = self.new_project_ref(domain_id=self.domainA['id']) + self.projectC = unit.new_project_ref(domain_id=self.domainA['id']) self.resource_api.create_project(self.projectC['id'], self.projectC) - self.user4 = self.new_user_ref(domain_id=self.domainB['id']) - password = self.user4['password'] - self.user4 = self.identity_api.create_user(self.user4) - self.user4['password'] = password - self.user5 = self.new_user_ref( - domain_id=self.domainA['id']) - password = self.user5['password'] - self.user5 = self.identity_api.create_user(self.user5) - self.user5['password'] = password - self.user6 = self.new_user_ref( - domain_id=self.domainA['id']) - password = self.user6['password'] - self.user6 = self.identity_api.create_user(self.user6) - self.user6['password'] = password + self.user4 = unit.create_user(self.identity_api, + domain_id=self.domainB['id']) + self.user5 = unit.create_user(self.identity_api, + domain_id=self.domainA['id']) + self.user6 = unit.create_user(self.identity_api, + domain_id=self.domainA['id']) self.identity_api.add_user_to_group(self.user5['id'], self.group1['id']) self.assignment_api.create_grant(self.role1['id'], @@ -954,29 +1691,29 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): def test_deleting_role_revokes_token(self): """Test deleting a role revokes token. - Add some additional test data, namely: - - A third project (project C) - - Three additional users - user4 owned by domainB and user5 and 6 - owned by domainA (different domain ownership should not affect - the test results, just provided to broaden test coverage) - - User5 is a member of group1 - - Group1 gets an additional assignment - role1 on projectB as - well as its existing role1 on projectA - - User4 has role2 on Project C - - User6 has role1 on projectA and domainA - - This allows us to create 5 tokens by virtue of different types - of role assignment: - - user1, scoped to ProjectA by virtue of user role1 assignment - - user5, scoped to ProjectB by virtue of group role1 assignment - - user4, scoped to ProjectC by virtue of user role2 assignment - - user6, scoped to ProjectA by virtue of user role1 assignment - - user6, scoped to DomainA by virtue of user role1 assignment - - role1 is then deleted - - Check the tokens on Project A and B, and DomainA are revoked, - but not the one for Project C + Add some additional test data, namely: + + - A third project (project C) + - Three additional users - user4 owned by domainB and user5 and 6 owned + by domainA (different domain ownership should not affect the test + results, just provided to broaden test coverage) + - User5 is a member of group1 + - Group1 gets an additional assignment - role1 on projectB as well as + its existing role1 on projectA + - User4 has role2 on Project C + - User6 has role1 on projectA and domainA + - This allows us to create 5 tokens by virtue of different types of + role assignment: + - user1, scoped to ProjectA by virtue of user role1 assignment + - user5, scoped to ProjectB by virtue of group role1 assignment + - user4, scoped to ProjectC by virtue of user role2 assignment + - user6, scoped to ProjectA by virtue of user role1 assignment + - user6, scoped to DomainA by virtue of user role1 assignment + - role1 is then deleted + - Check the tokens on Project A and B, and DomainA are revoked, but not + the one for Project C """ - self.role_data_fixtures() # Now we are ready to start issuing requests @@ -1008,19 +1745,19 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): # Confirm tokens are valid self.head('/auth/tokens', headers={'X-Subject-Token': tokenA}, - expected_status=200) + expected_status=http_client.OK) self.head('/auth/tokens', headers={'X-Subject-Token': tokenB}, - expected_status=200) + expected_status=http_client.OK) self.head('/auth/tokens', headers={'X-Subject-Token': tokenC}, - expected_status=200) + expected_status=http_client.OK) self.head('/auth/tokens', headers={'X-Subject-Token': tokenD}, - expected_status=200) + expected_status=http_client.OK) self.head('/auth/tokens', headers={'X-Subject-Token': tokenE}, - expected_status=200) + expected_status=http_client.OK) # Delete the role, which should invalidate the tokens role_url = '/roles/%s' % self.role1['id'] @@ -1043,7 +1780,7 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): # ...but the one using role2 is still valid self.head('/auth/tokens', headers={'X-Subject-Token': tokenC}, - expected_status=200) + expected_status=http_client.OK) def test_domain_user_role_assignment_maintains_token(self): """Test user-domain role assignment maintains existing token. @@ -1063,7 +1800,7 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): # Confirm token is valid self.head('/auth/tokens', headers={'X-Subject-Token': token}, - expected_status=200) + expected_status=http_client.OK) # Assign a role, which should not affect the token grant_url = ( '/domains/%(domain_id)s/users/%(user_id)s/' @@ -1074,7 +1811,7 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): self.put(grant_url) self.head('/auth/tokens', headers={'X-Subject-Token': token}, - expected_status=200) + expected_status=http_client.OK) def test_disabling_project_revokes_token(self): token = self.get_requested_token( @@ -1086,7 +1823,7 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): # confirm token is valid self.head('/auth/tokens', headers={'X-Subject-Token': token}, - expected_status=200) + expected_status=http_client.OK) # disable the project, which should invalidate the token self.patch( @@ -1097,7 +1834,7 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): self.head('/auth/tokens', headers={'X-Subject-Token': token}, expected_status=http_client.NOT_FOUND) - self.v3_authenticate_token( + self.v3_create_token( self.build_authentication_request( user_id=self.user3['id'], password=self.user3['password'], @@ -1114,7 +1851,7 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): # confirm token is valid self.head('/auth/tokens', headers={'X-Subject-Token': token}, - expected_status=200) + expected_status=http_client.OK) # delete the project, which should invalidate the token self.delete( @@ -1124,7 +1861,7 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): self.head('/auth/tokens', headers={'X-Subject-Token': token}, expected_status=http_client.NOT_FOUND) - self.v3_authenticate_token( + self.v3_create_token( self.build_authentication_request( user_id=self.user3['id'], password=self.user3['password'], @@ -1163,13 +1900,13 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): # Confirm tokens are valid self.head('/auth/tokens', headers={'X-Subject-Token': token1}, - expected_status=200) + expected_status=http_client.OK) self.head('/auth/tokens', headers={'X-Subject-Token': token2}, - expected_status=200) + expected_status=http_client.OK) self.head('/auth/tokens', headers={'X-Subject-Token': token3}, - expected_status=200) + expected_status=http_client.OK) # Delete the group grant, which should invalidate the # tokens for user1 and user2 grant_url = ( @@ -1209,7 +1946,7 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): # Confirm token is valid self.head('/auth/tokens', headers={'X-Subject-Token': token}, - expected_status=200) + expected_status=http_client.OK) # Delete the grant, which should invalidate the token grant_url = ( '/domains/%(domain_id)s/groups/%(group_id)s/' @@ -1220,7 +1957,7 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): self.put(grant_url) self.head('/auth/tokens', headers={'X-Subject-Token': token}, - expected_status=200) + expected_status=http_client.OK) def test_group_membership_changes_revokes_token(self): """Test add/removal to/from group revokes token. @@ -1250,10 +1987,10 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): # Confirm tokens are valid self.head('/auth/tokens', headers={'X-Subject-Token': token1}, - expected_status=200) + expected_status=http_client.OK) self.head('/auth/tokens', headers={'X-Subject-Token': token2}, - expected_status=200) + expected_status=http_client.OK) # Remove user1 from group1, which should invalidate # the token self.delete('/groups/%(group_id)s/users/%(user_id)s' % { @@ -1265,18 +2002,17 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): # But user2's token should still be valid self.head('/auth/tokens', headers={'X-Subject-Token': token2}, - expected_status=200) + expected_status=http_client.OK) # Adding user2 to a group should not invalidate token self.put('/groups/%(group_id)s/users/%(user_id)s' % { 'group_id': self.group2['id'], 'user_id': self.user2['id']}) self.head('/auth/tokens', headers={'X-Subject-Token': token2}, - expected_status=200) + expected_status=http_client.OK) def test_removing_role_assignment_does_not_affect_other_users(self): """Revoking a role from one user should not affect other users.""" - # This group grant is not needed for the test self.delete( '/projects/%(project_id)s/groups/%(group_id)s/roles/%(role_id)s' % @@ -1306,7 +2042,7 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): self.head('/auth/tokens', headers={'X-Subject-Token': user1_token}, expected_status=http_client.NOT_FOUND) - self.v3_authenticate_token( + self.v3_create_token( self.build_authentication_request( user_id=self.user1['id'], password=self.user1['password'], @@ -1316,8 +2052,8 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): # authorization for the second user should still succeed self.head('/auth/tokens', headers={'X-Subject-Token': user3_token}, - expected_status=200) - self.v3_authenticate_token( + expected_status=http_client.OK) + self.v3_create_token( self.build_authentication_request( user_id=self.user3['id'], password=self.user3['password'], @@ -1338,7 +2074,7 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): self.delete( '/projects/%(project_id)s' % {'project_id': self.projectA['id']}) - # Make sure that we get a NotFound(404) when heading that role. + # Make sure that we get a 404 Not Found when heading that role. self.head(role_path, expected_status=http_client.NOT_FOUND) def get_v2_token(self, token=None, project_id=None): @@ -1366,8 +2102,7 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): token = self.get_v2_token() self.delete('/auth/tokens', - headers={'X-Subject-Token': token}, - expected_status=204) + headers={'X-Subject-Token': token}) self.head('/auth/tokens', headers={'X-Subject-Token': token}, @@ -1397,8 +2132,7 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): # revoke the project-scoped token. self.delete('/auth/tokens', - headers={'X-Subject-Token': project_scoped_token}, - expected_status=204) + headers={'X-Subject-Token': project_scoped_token}) # The project-scoped token is invalidated. self.head('/auth/tokens', @@ -1408,17 +2142,16 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): # The unscoped token should still be valid. self.head('/auth/tokens', headers={'X-Subject-Token': unscoped_token}, - expected_status=200) + expected_status=http_client.OK) # The domain-scoped token should still be valid. self.head('/auth/tokens', headers={'X-Subject-Token': domain_scoped_token}, - expected_status=200) + expected_status=http_client.OK) # revoke the domain-scoped token. self.delete('/auth/tokens', - headers={'X-Subject-Token': domain_scoped_token}, - expected_status=204) + headers={'X-Subject-Token': domain_scoped_token}) # The domain-scoped token is invalid. self.head('/auth/tokens', @@ -1428,16 +2161,13 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): # The unscoped token should still be valid. self.head('/auth/tokens', headers={'X-Subject-Token': unscoped_token}, - expected_status=200) + expected_status=http_client.OK) def test_revoke_token_from_token_v2(self): # Test that a scoped token can be requested from an unscoped token, # the scoped token can be revoked, and the unscoped token remains # valid. - # FIXME(blk-u): This isn't working correctly. The scoped token should - # be revoked. See bug 1347318. - unscoped_token = self.get_v2_token() # Get a project-scoped token from the unscoped token @@ -1446,8 +2176,7 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): # revoke the project-scoped token. self.delete('/auth/tokens', - headers={'X-Subject-Token': project_scoped_token}, - expected_status=204) + headers={'X-Subject-Token': project_scoped_token}) # The project-scoped token is invalidated. self.head('/auth/tokens', @@ -1457,16 +2186,13 @@ class TestTokenRevokeById(test_v3.RestfulTestCase): # The unscoped token should still be valid. self.head('/auth/tokens', headers={'X-Subject-Token': unscoped_token}, - expected_status=200) + expected_status=http_client.OK) class TestTokenRevokeByAssignment(TestTokenRevokeById): def config_overrides(self): super(TestTokenRevokeById, self).config_overrides() - self.config_fixture.config( - group='revoke', - driver='kvs') self.config_fixture.config( group='token', provider='uuid', @@ -1501,7 +2227,7 @@ class TestTokenRevokeByAssignment(TestTokenRevokeById): # authorization for the projectA should still succeed self.head('/auth/tokens', headers={'X-Subject-Token': other_project_token}, - expected_status=200) + expected_status=http_client.OK) # while token for the projectB should not self.head('/auth/tokens', headers={'X-Subject-Token': project_token}, @@ -1512,14 +2238,21 @@ class TestTokenRevokeByAssignment(TestTokenRevokeById): self.assertIn(project_token, revoked_tokens) -class TestTokenRevokeApi(TestTokenRevokeById): - EXTENSION_NAME = 'revoke' - EXTENSION_TO_ADD = 'revoke_extension' +class RevokeContribTests(test_v3.RestfulTestCase): + @mock.patch.object(versionutils, 'report_deprecated_feature') + def test_exception_happens(self, mock_deprecator): + routers.RevokeExtension(mock.ANY) + mock_deprecator.assert_called_once_with(mock.ANY, mock.ANY) + args, _kwargs = mock_deprecator.call_args + self.assertIn("Remove revoke_extension from", args[1]) + + +class TestTokenRevokeApi(TestTokenRevokeById): """Test token revocation on the v3 Identity API.""" + def config_overrides(self): super(TestTokenRevokeApi, self).config_overrides() - self.config_fixture.config(group='revoke', driver='kvs') self.config_fixture.config( group='token', provider='pki', @@ -1536,15 +2269,19 @@ class TestTokenRevokeApi(TestTokenRevokeById): expected_response = {'events': [{'project_id': project_id}]} self.assertEqual(expected_response, events_response) - def assertDomainInList(self, events_response, domain_id): + def assertDomainAndProjectInList(self, events_response, domain_id): events = events_response['events'] - self.assertEqual(1, len(events)) - self.assertEqual(domain_id, events[0]['domain_id']) + self.assertEqual(2, len(events)) + self.assertEqual(domain_id, events[0]['project_id']) + self.assertEqual(domain_id, events[1]['domain_id']) self.assertIsNotNone(events[0]['issued_before']) + self.assertIsNotNone(events[1]['issued_before']) self.assertIsNotNone(events_response['links']) del (events_response['events'][0]['issued_before']) + del (events_response['events'][1]['issued_before']) del (events_response['links']) - expected_response = {'events': [{'domain_id': domain_id}]} + expected_response = {'events': [{'project_id': domain_id}, + {'domain_id': domain_id}]} self.assertEqual(expected_response, events_response) def assertValidRevokedTokenResponse(self, events_response, **kwargs): @@ -1563,62 +2300,55 @@ class TestTokenRevokeApi(TestTokenRevokeById): def test_revoke_token(self): scoped_token = self.get_scoped_token() headers = {'X-Subject-Token': scoped_token} - response = self.get('/auth/tokens', headers=headers, - expected_status=200).json_body['token'] + response = self.get('/auth/tokens', headers=headers).json_body['token'] - self.delete('/auth/tokens', headers=headers, expected_status=204) + self.delete('/auth/tokens', headers=headers) self.head('/auth/tokens', headers=headers, expected_status=http_client.NOT_FOUND) - events_response = self.get('/OS-REVOKE/events', - expected_status=200).json_body + events_response = self.get('/OS-REVOKE/events').json_body self.assertValidRevokedTokenResponse(events_response, audit_id=response['audit_ids'][0]) def test_revoke_v2_token(self): token = self.get_v2_token() headers = {'X-Subject-Token': token} - response = self.get('/auth/tokens', headers=headers, - expected_status=200).json_body['token'] - self.delete('/auth/tokens', headers=headers, expected_status=204) + response = self.get('/auth/tokens', + headers=headers).json_body['token'] + self.delete('/auth/tokens', headers=headers) self.head('/auth/tokens', headers=headers, expected_status=http_client.NOT_FOUND) - events_response = self.get('/OS-REVOKE/events', - expected_status=200).json_body + events_response = self.get('/OS-REVOKE/events').json_body self.assertValidRevokedTokenResponse( events_response, audit_id=response['audit_ids'][0]) - def test_revoke_by_id_false_410(self): + def test_revoke_by_id_false_returns_gone(self): self.get('/auth/tokens/OS-PKI/revoked', expected_status=http_client.GONE) def test_list_delete_project_shows_in_event_list(self): self.role_data_fixtures() - events = self.get('/OS-REVOKE/events', - expected_status=200).json_body['events'] + events = self.get('/OS-REVOKE/events').json_body['events'] self.assertEqual([], events) self.delete( '/projects/%(project_id)s' % {'project_id': self.projectA['id']}) - events_response = self.get('/OS-REVOKE/events', - expected_status=200).json_body + events_response = self.get('/OS-REVOKE/events').json_body self.assertValidDeletedProjectResponse(events_response, self.projectA['id']) def test_disable_domain_shows_in_event_list(self): - events = self.get('/OS-REVOKE/events', - expected_status=200).json_body['events'] + events = self.get('/OS-REVOKE/events').json_body['events'] self.assertEqual([], events) disable_body = {'domain': {'enabled': False}} self.patch( '/domains/%(project_id)s' % {'project_id': self.domainA['id']}, body=disable_body) - events = self.get('/OS-REVOKE/events', - expected_status=200).json_body + events = self.get('/OS-REVOKE/events').json_body - self.assertDomainInList(events, self.domainA['id']) + self.assertDomainAndProjectInList(events, self.domainA['id']) def assertEventDataInList(self, events, **kwargs): found = False @@ -1646,30 +2376,31 @@ class TestTokenRevokeApi(TestTokenRevokeById): def test_list_delete_token_shows_in_event_list(self): self.role_data_fixtures() - events = self.get('/OS-REVOKE/events', - expected_status=200).json_body['events'] + events = self.get('/OS-REVOKE/events').json_body['events'] self.assertEqual([], events) scoped_token = self.get_scoped_token() headers = {'X-Subject-Token': scoped_token} auth_req = self.build_authentication_request(token=scoped_token) - response = self.v3_authenticate_token(auth_req) + response = self.v3_create_token(auth_req) token2 = response.json_body['token'] headers2 = {'X-Subject-Token': response.headers['X-Subject-Token']} - response = self.v3_authenticate_token(auth_req) + response = self.v3_create_token(auth_req) response.json_body['token'] headers3 = {'X-Subject-Token': response.headers['X-Subject-Token']} - self.head('/auth/tokens', headers=headers, expected_status=200) - self.head('/auth/tokens', headers=headers2, expected_status=200) - self.head('/auth/tokens', headers=headers3, expected_status=200) + self.head('/auth/tokens', headers=headers, + expected_status=http_client.OK) + self.head('/auth/tokens', headers=headers2, + expected_status=http_client.OK) + self.head('/auth/tokens', headers=headers3, + expected_status=http_client.OK) - self.delete('/auth/tokens', headers=headers, expected_status=204) + self.delete('/auth/tokens', headers=headers) # NOTE(ayoung): not deleting token3, as it should be deleted # by previous - events_response = self.get('/OS-REVOKE/events', - expected_status=200).json_body + events_response = self.get('/OS-REVOKE/events').json_body events = events_response['events'] self.assertEqual(1, len(events)) self.assertEventDataInList( @@ -1677,32 +2408,32 @@ class TestTokenRevokeApi(TestTokenRevokeById): audit_id=token2['audit_ids'][1]) self.head('/auth/tokens', headers=headers, expected_status=http_client.NOT_FOUND) - self.head('/auth/tokens', headers=headers2, expected_status=200) - self.head('/auth/tokens', headers=headers3, expected_status=200) + self.head('/auth/tokens', headers=headers2, + expected_status=http_client.OK) + self.head('/auth/tokens', headers=headers3, + expected_status=http_client.OK) def test_list_with_filter(self): self.role_data_fixtures() - events = self.get('/OS-REVOKE/events', - expected_status=200).json_body['events'] + events = self.get('/OS-REVOKE/events').json_body['events'] self.assertEqual(0, len(events)) scoped_token = self.get_scoped_token() headers = {'X-Subject-Token': scoped_token} auth = self.build_authentication_request(token=scoped_token) headers2 = {'X-Subject-Token': self.get_requested_token(auth)} - self.delete('/auth/tokens', headers=headers, expected_status=204) - self.delete('/auth/tokens', headers=headers2, expected_status=204) + self.delete('/auth/tokens', headers=headers) + self.delete('/auth/tokens', headers=headers2) - events = self.get('/OS-REVOKE/events', - expected_status=200).json_body['events'] + events = self.get('/OS-REVOKE/events').json_body['events'] self.assertEqual(2, len(events)) future = utils.isotime(timeutils.utcnow() + datetime.timedelta(seconds=1000)) - events = self.get('/OS-REVOKE/events?since=%s' % (future), - expected_status=200).json_body['events'] + events = self.get('/OS-REVOKE/events?since=%s' % (future) + ).json_body['events'] self.assertEqual(0, len(events)) @@ -1764,7 +2495,7 @@ class TestAuthExternalDomain(test_v3.RestfulTestCase): self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, 'REMOTE_DOMAIN': remote_domain, 'AUTH_TYPE': 'Negotiate'}) - r = self.v3_authenticate_token(auth_data) + r = self.v3_create_token(auth_data) token = self.assertValidProjectScopedTokenResponse(r) self.assertEqual(self.user['name'], token['bind']['kerberos']) @@ -1776,7 +2507,7 @@ class TestAuthExternalDomain(test_v3.RestfulTestCase): self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, 'REMOTE_DOMAIN': remote_domain, 'AUTH_TYPE': 'Negotiate'}) - r = self.v3_authenticate_token(auth_data) + r = self.v3_create_token(auth_data) token = self.assertValidUnscopedTokenResponse(r) self.assertEqual(self.user['name'], token['bind']['kerberos']) @@ -1820,7 +2551,7 @@ class TestAuthExternalDefaultDomain(test_v3.RestfulTestCase): remote_user = self.default_domain_user['name'] self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, 'AUTH_TYPE': 'Negotiate'}) - r = self.v3_authenticate_token(auth_data) + r = self.v3_create_token(auth_data) token = self.assertValidProjectScopedTokenResponse(r) self.assertEqual(self.default_domain_user['name'], token['bind']['kerberos']) @@ -1831,7 +2562,7 @@ class TestAuthExternalDefaultDomain(test_v3.RestfulTestCase): remote_user = self.default_domain_user['name'] self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, 'AUTH_TYPE': 'Negotiate'}) - r = self.v3_authenticate_token(auth_data) + r = self.v3_create_token(auth_data) token = self.assertValidUnscopedTokenResponse(r) self.assertEqual(self.default_domain_user['name'], token['bind']['kerberos']) @@ -1852,7 +2583,7 @@ class TestAuth(test_v3.RestfulTestCase): auth_data = self.build_authentication_request( user_id=self.user['id'], password=self.user['password']) - r = self.v3_authenticate_token(auth_data) + r = self.v3_create_token(auth_data) self.assertValidUnscopedTokenResponse(r) def test_unscoped_token_with_user_domain_id(self): @@ -1860,7 +2591,7 @@ class TestAuth(test_v3.RestfulTestCase): username=self.user['name'], user_domain_id=self.domain['id'], password=self.user['password']) - r = self.v3_authenticate_token(auth_data) + r = self.v3_create_token(auth_data) self.assertValidUnscopedTokenResponse(r) def test_unscoped_token_with_user_domain_name(self): @@ -1868,7 +2599,7 @@ class TestAuth(test_v3.RestfulTestCase): username=self.user['name'], user_domain_name=self.domain['name'], password=self.user['password']) - r = self.v3_authenticate_token(auth_data) + r = self.v3_create_token(auth_data) self.assertValidUnscopedTokenResponse(r) def test_project_id_scoped_token_with_user_id(self): @@ -1876,11 +2607,11 @@ class TestAuth(test_v3.RestfulTestCase): user_id=self.user['id'], password=self.user['password'], project_id=self.project['id']) - r = self.v3_authenticate_token(auth_data) + r = self.v3_create_token(auth_data) self.assertValidProjectScopedTokenResponse(r) def _second_project_as_default(self): - ref = self.new_project_ref(domain_id=self.domain_id) + ref = unit.new_project_ref(domain_id=self.domain_id) r = self.post('/projects', body={'project': ref}) project = self.assertValidProjectResponse(r, ref) @@ -1907,7 +2638,7 @@ class TestAuth(test_v3.RestfulTestCase): auth_data = self.build_authentication_request( user_id=self.user['id'], password=self.user['password']) - r = self.v3_authenticate_token(auth_data) + r = self.v3_create_token(auth_data) self.assertValidProjectScopedTokenResponse(r) self.assertEqual(project['id'], r.result['token']['project']['id']) @@ -1952,7 +2683,7 @@ class TestAuth(test_v3.RestfulTestCase): user_id=self.user['id'], password=self.user['password'], project_id=self.project['id']) - r = self.v3_authenticate_token(auth_data) + r = self.v3_create_token(auth_data) catalog = r.result['token']['catalog'] self.assertEqual(1, len(catalog)) @@ -1989,13 +2720,12 @@ class TestAuth(test_v3.RestfulTestCase): user_id=self.user['id'], password=self.user['password'], project_id=self.project['id']) - r = self.v3_authenticate_token(auth_data) + r = self.v3_create_token(auth_data) self.assertEqual([], r.result['token']['catalog']) def test_auth_catalog_disabled_endpoint(self): """On authenticate, get a catalog that excludes disabled endpoints.""" - # Create a disabled endpoint that's like the enabled one. disabled_endpoint_ref = copy.copy(self.endpoint) disabled_endpoint_id = uuid.uuid4().hex @@ -2011,21 +2741,21 @@ class TestAuth(test_v3.RestfulTestCase): user_id=self.user['id'], password=self.user['password'], project_id=self.project['id']) - r = self.v3_authenticate_token(auth_data) + r = self.v3_create_token(auth_data) self._check_disabled_endpoint_result(r.result['token']['catalog'], disabled_endpoint_id) def test_project_id_scoped_token_with_user_id_unauthorized(self): - project = self.new_project_ref(domain_id=self.domain_id) + project = unit.new_project_ref(domain_id=self.domain_id) self.resource_api.create_project(project['id'], project) auth_data = self.build_authentication_request( user_id=self.user['id'], password=self.user['password'], project_id=project['id']) - self.v3_authenticate_token(auth_data, - expected_status=http_client.UNAUTHORIZED) + self.v3_create_token(auth_data, + expected_status=http_client.UNAUTHORIZED) def test_user_and_group_roles_scoped_token(self): """Test correct roles are returned in scoped token. @@ -2049,30 +2779,19 @@ class TestAuth(test_v3.RestfulTestCase): tokens """ - - domainA = self.new_domain_ref() + domainA = unit.new_domain_ref() self.resource_api.create_domain(domainA['id'], domainA) - projectA = self.new_project_ref(domain_id=domainA['id']) + projectA = unit.new_project_ref(domain_id=domainA['id']) self.resource_api.create_project(projectA['id'], projectA) - user1 = self.new_user_ref( - domain_id=domainA['id']) - password = user1['password'] - user1 = self.identity_api.create_user(user1) - user1['password'] = password + user1 = unit.create_user(self.identity_api, domain_id=domainA['id']) - user2 = self.new_user_ref( - domain_id=domainA['id']) - password = user2['password'] - user2 = self.identity_api.create_user(user2) - user2['password'] = password + user2 = unit.create_user(self.identity_api, domain_id=domainA['id']) - group1 = self.new_group_ref( - domain_id=domainA['id']) + group1 = unit.new_group_ref(domain_id=domainA['id']) group1 = self.identity_api.create_group(group1) - group2 = self.new_group_ref( - domain_id=domainA['id']) + group2 = unit.new_group_ref(domain_id=domainA['id']) group2 = self.identity_api.create_group(group2) self.identity_api.add_user_to_group(user1['id'], @@ -2083,7 +2802,7 @@ class TestAuth(test_v3.RestfulTestCase): # Now create all the roles and assign them role_list = [] for _ in range(8): - role = self.new_role_ref() + role = unit.new_role_ref() self.role_api.create_role(role['id'], role) role_list.append(role) @@ -2119,7 +2838,7 @@ class TestAuth(test_v3.RestfulTestCase): user_id=user1['id'], password=user1['password'], project_id=projectA['id']) - r = self.v3_authenticate_token(auth_data) + r = self.v3_create_token(auth_data) token = self.assertValidScopedTokenResponse(r) roles_ids = [] for ref in token['roles']: @@ -2133,7 +2852,7 @@ class TestAuth(test_v3.RestfulTestCase): user_id=user1['id'], password=user1['password'], domain_id=domainA['id']) - r = self.v3_authenticate_token(auth_data) + r = self.v3_create_token(auth_data) token = self.assertValidScopedTokenResponse(r) roles_ids = [] for ref in token['roles']: @@ -2151,7 +2870,7 @@ class TestAuth(test_v3.RestfulTestCase): user_id=user1['id'], password=user1['password'], project_id=projectA['id']) - r = self.v3_authenticate_token(auth_data) + r = self.v3_create_token(auth_data) token = self.assertValidScopedTokenResponse(r) roles_ids = [] for ref in token['roles']: @@ -2164,30 +2883,23 @@ class TestAuth(test_v3.RestfulTestCase): def test_auth_token_cross_domain_group_and_project(self): """Verify getting a token in cross domain group/project roles.""" # create domain, project and group and grant roles to user - domain1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + domain1 = unit.new_domain_ref() self.resource_api.create_domain(domain1['id'], domain1) - project1 = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex, - 'domain_id': domain1['id']} + project1 = unit.new_project_ref(domain_id=domain1['id']) self.resource_api.create_project(project1['id'], project1) - user_foo = self.new_user_ref(domain_id=test_v3.DEFAULT_DOMAIN_ID) - password = user_foo['password'] - user_foo = self.identity_api.create_user(user_foo) - user_foo['password'] = password - role_member = {'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex} + user_foo = unit.create_user(self.identity_api, + domain_id=test_v3.DEFAULT_DOMAIN_ID) + role_member = unit.new_role_ref() self.role_api.create_role(role_member['id'], role_member) - role_admin = {'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex} + role_admin = unit.new_role_ref() self.role_api.create_role(role_admin['id'], role_admin) - role_foo_domain1 = {'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex} + role_foo_domain1 = unit.new_role_ref() self.role_api.create_role(role_foo_domain1['id'], role_foo_domain1) - role_group_domain1 = {'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex} + role_group_domain1 = unit.new_role_ref() self.role_api.create_role(role_group_domain1['id'], role_group_domain1) self.assignment_api.add_user_to_project(project1['id'], user_foo['id']) - new_group = {'domain_id': domain1['id'], 'name': uuid.uuid4().hex} + new_group = unit.new_group_ref(domain_id=domain1['id']) new_group = self.identity_api.create_group(new_group) self.identity_api.add_user_to_group(user_foo['id'], new_group['id']) @@ -2208,1435 +2920,1072 @@ class TestAuth(test_v3.RestfulTestCase): domain_id=domain1['id'], role_id=role_group_domain1['id']) - # Get a scoped token for the project - auth_data = self.build_authentication_request( - username=user_foo['name'], - user_domain_id=test_v3.DEFAULT_DOMAIN_ID, - password=user_foo['password'], - project_name=project1['name'], - project_domain_id=domain1['id']) - - r = self.v3_authenticate_token(auth_data) - scoped_token = self.assertValidScopedTokenResponse(r) - project = scoped_token["project"] - roles_ids = [] - for ref in scoped_token['roles']: - roles_ids.append(ref['id']) - self.assertEqual(project1['id'], project["id"]) - self.assertIn(role_member['id'], roles_ids) - self.assertIn(role_admin['id'], roles_ids) - self.assertNotIn(role_foo_domain1['id'], roles_ids) - self.assertNotIn(role_group_domain1['id'], roles_ids) - - def test_project_id_scoped_token_with_user_domain_id(self): - auth_data = self.build_authentication_request( - username=self.user['name'], - user_domain_id=self.domain['id'], - password=self.user['password'], - project_id=self.project['id']) - r = self.v3_authenticate_token(auth_data) - self.assertValidProjectScopedTokenResponse(r) - - def test_project_id_scoped_token_with_user_domain_name(self): - auth_data = self.build_authentication_request( - username=self.user['name'], - user_domain_name=self.domain['name'], - password=self.user['password'], - project_id=self.project['id']) - r = self.v3_authenticate_token(auth_data) - self.assertValidProjectScopedTokenResponse(r) - - def test_domain_id_scoped_token_with_user_id(self): - path = '/domains/%s/users/%s/roles/%s' % ( - self.domain['id'], self.user['id'], self.role['id']) - self.put(path=path) - - auth_data = self.build_authentication_request( - user_id=self.user['id'], - password=self.user['password'], - domain_id=self.domain['id']) - r = self.v3_authenticate_token(auth_data) - self.assertValidDomainScopedTokenResponse(r) - - def test_domain_id_scoped_token_with_user_domain_id(self): - path = '/domains/%s/users/%s/roles/%s' % ( - self.domain['id'], self.user['id'], self.role['id']) - self.put(path=path) - - auth_data = self.build_authentication_request( - username=self.user['name'], - user_domain_id=self.domain['id'], - password=self.user['password'], - domain_id=self.domain['id']) - r = self.v3_authenticate_token(auth_data) - self.assertValidDomainScopedTokenResponse(r) - - def test_domain_id_scoped_token_with_user_domain_name(self): - path = '/domains/%s/users/%s/roles/%s' % ( - self.domain['id'], self.user['id'], self.role['id']) - self.put(path=path) - - auth_data = self.build_authentication_request( - username=self.user['name'], - user_domain_name=self.domain['name'], - password=self.user['password'], - domain_id=self.domain['id']) - r = self.v3_authenticate_token(auth_data) - self.assertValidDomainScopedTokenResponse(r) - - def test_domain_name_scoped_token_with_user_id(self): - path = '/domains/%s/users/%s/roles/%s' % ( - self.domain['id'], self.user['id'], self.role['id']) - self.put(path=path) - - auth_data = self.build_authentication_request( - user_id=self.user['id'], - password=self.user['password'], - domain_name=self.domain['name']) - r = self.v3_authenticate_token(auth_data) - self.assertValidDomainScopedTokenResponse(r) - - def test_domain_name_scoped_token_with_user_domain_id(self): - path = '/domains/%s/users/%s/roles/%s' % ( - self.domain['id'], self.user['id'], self.role['id']) - self.put(path=path) - - auth_data = self.build_authentication_request( - username=self.user['name'], - user_domain_id=self.domain['id'], - password=self.user['password'], - domain_name=self.domain['name']) - r = self.v3_authenticate_token(auth_data) - self.assertValidDomainScopedTokenResponse(r) - - def test_domain_name_scoped_token_with_user_domain_name(self): - path = '/domains/%s/users/%s/roles/%s' % ( - self.domain['id'], self.user['id'], self.role['id']) - self.put(path=path) - - auth_data = self.build_authentication_request( - username=self.user['name'], - user_domain_name=self.domain['name'], - password=self.user['password'], - domain_name=self.domain['name']) - r = self.v3_authenticate_token(auth_data) - self.assertValidDomainScopedTokenResponse(r) - - def test_domain_scope_token_with_group_role(self): - group = self.new_group_ref( - domain_id=self.domain_id) - group = self.identity_api.create_group(group) - - # add user to group - self.identity_api.add_user_to_group(self.user['id'], group['id']) - - # grant the domain role to group - path = '/domains/%s/groups/%s/roles/%s' % ( - self.domain['id'], group['id'], self.role['id']) - self.put(path=path) - - # now get a domain-scoped token - auth_data = self.build_authentication_request( - user_id=self.user['id'], - password=self.user['password'], - domain_id=self.domain['id']) - r = self.v3_authenticate_token(auth_data) - self.assertValidDomainScopedTokenResponse(r) - - def test_domain_scope_token_with_name(self): - # grant the domain role to user - path = '/domains/%s/users/%s/roles/%s' % ( - self.domain['id'], self.user['id'], self.role['id']) - self.put(path=path) - # now get a domain-scoped token - auth_data = self.build_authentication_request( - user_id=self.user['id'], - password=self.user['password'], - domain_name=self.domain['name']) - r = self.v3_authenticate_token(auth_data) - self.assertValidDomainScopedTokenResponse(r) - - def test_domain_scope_failed(self): - auth_data = self.build_authentication_request( - user_id=self.user['id'], - password=self.user['password'], - domain_id=self.domain['id']) - self.v3_authenticate_token(auth_data, - expected_status=http_client.UNAUTHORIZED) - - def test_auth_with_id(self): - auth_data = self.build_authentication_request( - user_id=self.user['id'], - password=self.user['password']) - r = self.v3_authenticate_token(auth_data) - self.assertValidUnscopedTokenResponse(r) - - token = r.headers.get('X-Subject-Token') - - # test token auth - auth_data = self.build_authentication_request(token=token) - r = self.v3_authenticate_token(auth_data) - self.assertValidUnscopedTokenResponse(r) - - def get_v2_token(self, tenant_id=None): - body = { - 'auth': { - 'passwordCredentials': { - 'username': self.default_domain_user['name'], - 'password': self.default_domain_user['password'], - }, - }, - } - r = self.admin_request(method='POST', path='/v2.0/tokens', body=body) - return r - - def test_validate_v2_unscoped_token_with_v3_api(self): - v2_token = self.get_v2_token().result['access']['token']['id'] - auth_data = self.build_authentication_request(token=v2_token) - r = self.v3_authenticate_token(auth_data) - self.assertValidUnscopedTokenResponse(r) - - def test_validate_v2_scoped_token_with_v3_api(self): - v2_response = self.get_v2_token( - tenant_id=self.default_domain_project['id']) - result = v2_response.result - v2_token = result['access']['token']['id'] - auth_data = self.build_authentication_request( - token=v2_token, - project_id=self.default_domain_project['id']) - r = self.v3_authenticate_token(auth_data) - self.assertValidScopedTokenResponse(r) - - def test_invalid_user_id(self): - auth_data = self.build_authentication_request( - user_id=uuid.uuid4().hex, - password=self.user['password']) - self.v3_authenticate_token(auth_data, - expected_status=http_client.UNAUTHORIZED) - - def test_invalid_user_name(self): - auth_data = self.build_authentication_request( - username=uuid.uuid4().hex, - user_domain_id=self.domain['id'], - password=self.user['password']) - self.v3_authenticate_token(auth_data, - expected_status=http_client.UNAUTHORIZED) - - def test_invalid_domain_id(self): - auth_data = self.build_authentication_request( - username=self.user['name'], - user_domain_id=uuid.uuid4().hex, - password=self.user['password']) - self.v3_authenticate_token(auth_data, - expected_status=http_client.UNAUTHORIZED) - - def test_invalid_domain_name(self): - auth_data = self.build_authentication_request( - username=self.user['name'], - user_domain_name=uuid.uuid4().hex, - password=self.user['password']) - self.v3_authenticate_token(auth_data, - expected_status=http_client.UNAUTHORIZED) - - def test_invalid_password(self): - auth_data = self.build_authentication_request( - user_id=self.user['id'], - password=uuid.uuid4().hex) - self.v3_authenticate_token(auth_data, - expected_status=http_client.UNAUTHORIZED) - - def test_remote_user_no_realm(self): - api = auth.controllers.Auth() - context, auth_info, auth_context = self.build_external_auth_request( - self.default_domain_user['name']) - api.authenticate(context, auth_info, auth_context) - self.assertEqual(self.default_domain_user['id'], - auth_context['user_id']) - # Now test to make sure the user name can, itself, contain the - # '@' character. - user = {'name': 'myname@mydivision'} - self.identity_api.update_user(self.default_domain_user['id'], user) - context, auth_info, auth_context = self.build_external_auth_request( - user["name"]) - api.authenticate(context, auth_info, auth_context) - self.assertEqual(self.default_domain_user['id'], - auth_context['user_id']) - - def test_remote_user_no_domain(self): - api = auth.controllers.Auth() - context, auth_info, auth_context = self.build_external_auth_request( - self.user['name']) - self.assertRaises(exception.Unauthorized, - api.authenticate, - context, - auth_info, - auth_context) - - def test_remote_user_and_password(self): - # both REMOTE_USER and password methods must pass. - # note that they do not have to match - api = auth.controllers.Auth() - auth_data = self.build_authentication_request( - user_domain_id=self.default_domain_user['domain_id'], - username=self.default_domain_user['name'], - password=self.default_domain_user['password'])['auth'] - context, auth_info, auth_context = self.build_external_auth_request( - self.default_domain_user['name'], auth_data=auth_data) - - api.authenticate(context, auth_info, auth_context) - - def test_remote_user_and_explicit_external(self): - # both REMOTE_USER and password methods must pass. - # note that they do not have to match - auth_data = self.build_authentication_request( - user_domain_id=self.domain['id'], - username=self.user['name'], - password=self.user['password'])['auth'] - auth_data['identity']['methods'] = ["password", "external"] - auth_data['identity']['external'] = {} - api = auth.controllers.Auth() - auth_info = auth.controllers.AuthInfo(None, auth_data) - auth_context = {'extras': {}, 'method_names': []} - self.assertRaises(exception.Unauthorized, - api.authenticate, - self.empty_context, - auth_info, - auth_context) - - def test_remote_user_bad_password(self): - # both REMOTE_USER and password methods must pass. - api = auth.controllers.Auth() - auth_data = self.build_authentication_request( - user_domain_id=self.domain['id'], - username=self.user['name'], - password='badpassword')['auth'] - context, auth_info, auth_context = self.build_external_auth_request( - self.default_domain_user['name'], auth_data=auth_data) - self.assertRaises(exception.Unauthorized, - api.authenticate, - context, - auth_info, - auth_context) - - def test_bind_not_set_with_remote_user(self): - self.config_fixture.config(group='token', bind=[]) - auth_data = self.build_authentication_request() - remote_user = self.default_domain_user['name'] - self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, - 'AUTH_TYPE': 'Negotiate'}) - r = self.v3_authenticate_token(auth_data) - token = self.assertValidUnscopedTokenResponse(r) - self.assertNotIn('bind', token) - - # TODO(ayoung): move to TestPKITokenAPIs; it will be run for both formats - def test_verify_with_bound_token(self): - self.config_fixture.config(group='token', bind='kerberos') + # Get a scoped token for the project auth_data = self.build_authentication_request( - project_id=self.project['id']) - remote_user = self.default_domain_user['name'] - self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, - 'AUTH_TYPE': 'Negotiate'}) + username=user_foo['name'], + user_domain_id=test_v3.DEFAULT_DOMAIN_ID, + password=user_foo['password'], + project_name=project1['name'], + project_domain_id=domain1['id']) - token = self.get_requested_token(auth_data) - headers = {'X-Subject-Token': token} - r = self.get('/auth/tokens', headers=headers, token=token) - token = self.assertValidProjectScopedTokenResponse(r) - self.assertEqual(self.default_domain_user['name'], - token['bind']['kerberos']) + r = self.v3_create_token(auth_data) + scoped_token = self.assertValidScopedTokenResponse(r) + project = scoped_token["project"] + roles_ids = [] + for ref in scoped_token['roles']: + roles_ids.append(ref['id']) + self.assertEqual(project1['id'], project["id"]) + self.assertIn(role_member['id'], roles_ids) + self.assertIn(role_admin['id'], roles_ids) + self.assertNotIn(role_foo_domain1['id'], roles_ids) + self.assertNotIn(role_group_domain1['id'], roles_ids) - def test_auth_with_bind_token(self): - self.config_fixture.config(group='token', bind=['kerberos']) + def test_project_id_scoped_token_with_user_domain_id(self): + auth_data = self.build_authentication_request( + username=self.user['name'], + user_domain_id=self.domain['id'], + password=self.user['password'], + project_id=self.project['id']) + r = self.v3_create_token(auth_data) + self.assertValidProjectScopedTokenResponse(r) - auth_data = self.build_authentication_request() - remote_user = self.default_domain_user['name'] - self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, - 'AUTH_TYPE': 'Negotiate'}) - r = self.v3_authenticate_token(auth_data) + def test_project_id_scoped_token_with_user_domain_name(self): + auth_data = self.build_authentication_request( + username=self.user['name'], + user_domain_name=self.domain['name'], + password=self.user['password'], + project_id=self.project['id']) + r = self.v3_create_token(auth_data) + self.assertValidProjectScopedTokenResponse(r) - # the unscoped token should have bind information in it - token = self.assertValidUnscopedTokenResponse(r) - self.assertEqual(remote_user, token['bind']['kerberos']) + def test_domain_id_scoped_token_with_user_id(self): + path = '/domains/%s/users/%s/roles/%s' % ( + self.domain['id'], self.user['id'], self.role['id']) + self.put(path=path) - token = r.headers.get('X-Subject-Token') + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + domain_id=self.domain['id']) + r = self.v3_create_token(auth_data) + self.assertValidDomainScopedTokenResponse(r) - # using unscoped token with remote user succeeds - auth_params = {'token': token, 'project_id': self.project_id} - auth_data = self.build_authentication_request(**auth_params) - r = self.v3_authenticate_token(auth_data) - token = self.assertValidProjectScopedTokenResponse(r) + def test_domain_id_scoped_token_with_user_domain_id(self): + path = '/domains/%s/users/%s/roles/%s' % ( + self.domain['id'], self.user['id'], self.role['id']) + self.put(path=path) - # the bind information should be carried over from the original token - self.assertEqual(remote_user, token['bind']['kerberos']) + auth_data = self.build_authentication_request( + username=self.user['name'], + user_domain_id=self.domain['id'], + password=self.user['password'], + domain_id=self.domain['id']) + r = self.v3_create_token(auth_data) + self.assertValidDomainScopedTokenResponse(r) - def test_v2_v3_bind_token_intermix(self): - self.config_fixture.config(group='token', bind='kerberos') + def test_domain_id_scoped_token_with_user_domain_name(self): + path = '/domains/%s/users/%s/roles/%s' % ( + self.domain['id'], self.user['id'], self.role['id']) + self.put(path=path) - # we need our own user registered to the default domain because of - # the way external auth works. - remote_user = self.default_domain_user['name'] - self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, - 'AUTH_TYPE': 'Negotiate'}) - body = {'auth': {}} - resp = self.admin_request(path='/v2.0/tokens', - method='POST', - body=body) + auth_data = self.build_authentication_request( + username=self.user['name'], + user_domain_name=self.domain['name'], + password=self.user['password'], + domain_id=self.domain['id']) + r = self.v3_create_token(auth_data) + self.assertValidDomainScopedTokenResponse(r) - v2_token_data = resp.result + def test_domain_name_scoped_token_with_user_id(self): + path = '/domains/%s/users/%s/roles/%s' % ( + self.domain['id'], self.user['id'], self.role['id']) + self.put(path=path) - bind = v2_token_data['access']['token']['bind'] - self.assertEqual(self.default_domain_user['name'], bind['kerberos']) + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + domain_name=self.domain['name']) + r = self.v3_create_token(auth_data) + self.assertValidDomainScopedTokenResponse(r) - v2_token_id = v2_token_data['access']['token']['id'] - # NOTE(gyee): self.get() will try to obtain an auth token if one - # is not provided. When REMOTE_USER is present in the request - # environment, the external user auth plugin is used in conjunction - # with the password auth for the admin user. Therefore, we need to - # cleanup the REMOTE_USER information from the previous call. - del self.admin_app.extra_environ['REMOTE_USER'] - headers = {'X-Subject-Token': v2_token_id} - resp = self.get('/auth/tokens', headers=headers) - token_data = resp.result + def test_domain_name_scoped_token_with_user_domain_id(self): + path = '/domains/%s/users/%s/roles/%s' % ( + self.domain['id'], self.user['id'], self.role['id']) + self.put(path=path) - self.assertDictEqual(v2_token_data['access']['token']['bind'], - token_data['token']['bind']) + auth_data = self.build_authentication_request( + username=self.user['name'], + user_domain_id=self.domain['id'], + password=self.user['password'], + domain_name=self.domain['name']) + r = self.v3_create_token(auth_data) + self.assertValidDomainScopedTokenResponse(r) - def test_authenticating_a_user_with_no_password(self): - user = self.new_user_ref(domain_id=self.domain['id']) - user.pop('password', None) # can't have a password for this test - user = self.identity_api.create_user(user) + def test_domain_name_scoped_token_with_user_domain_name(self): + path = '/domains/%s/users/%s/roles/%s' % ( + self.domain['id'], self.user['id'], self.role['id']) + self.put(path=path) auth_data = self.build_authentication_request( - user_id=user['id'], - password='password') + username=self.user['name'], + user_domain_name=self.domain['name'], + password=self.user['password'], + domain_name=self.domain['name']) + r = self.v3_create_token(auth_data) + self.assertValidDomainScopedTokenResponse(r) - self.v3_authenticate_token(auth_data, - expected_status=http_client.UNAUTHORIZED) + def test_domain_scope_token_with_group_role(self): + group = unit.new_group_ref(domain_id=self.domain_id) + group = self.identity_api.create_group(group) - def test_disabled_default_project_result_in_unscoped_token(self): - # create a disabled project to work with - project = self.create_new_default_project_for_user( - self.user['id'], self.domain_id, enable_project=False) + # add user to group + self.identity_api.add_user_to_group(self.user['id'], group['id']) - # assign a role to user for the new project - self.assignment_api.add_role_to_user_and_project(self.user['id'], - project['id'], - self.role_id) + # grant the domain role to group + path = '/domains/%s/groups/%s/roles/%s' % ( + self.domain['id'], group['id'], self.role['id']) + self.put(path=path) - # attempt to authenticate without requesting a project + # now get a domain-scoped token auth_data = self.build_authentication_request( user_id=self.user['id'], - password=self.user['password']) - r = self.v3_authenticate_token(auth_data) - self.assertValidUnscopedTokenResponse(r) - - def test_disabled_default_project_domain_result_in_unscoped_token(self): - domain_ref = self.new_domain_ref() - r = self.post('/domains', body={'domain': domain_ref}) - domain = self.assertValidDomainResponse(r, domain_ref) - - project = self.create_new_default_project_for_user( - self.user['id'], domain['id']) - - # assign a role to user for the new project - self.assignment_api.add_role_to_user_and_project(self.user['id'], - project['id'], - self.role_id) - - # now disable the project domain - body = {'domain': {'enabled': False}} - r = self.patch('/domains/%(domain_id)s' % {'domain_id': domain['id']}, - body=body) - self.assertValidDomainResponse(r) + password=self.user['password'], + domain_id=self.domain['id']) + r = self.v3_create_token(auth_data) + self.assertValidDomainScopedTokenResponse(r) - # attempt to authenticate without requesting a project + def test_domain_scope_token_with_name(self): + # grant the domain role to user + path = '/domains/%s/users/%s/roles/%s' % ( + self.domain['id'], self.user['id'], self.role['id']) + self.put(path=path) + # now get a domain-scoped token auth_data = self.build_authentication_request( user_id=self.user['id'], - password=self.user['password']) - r = self.v3_authenticate_token(auth_data) - self.assertValidUnscopedTokenResponse(r) + password=self.user['password'], + domain_name=self.domain['name']) + r = self.v3_create_token(auth_data) + self.assertValidDomainScopedTokenResponse(r) - def test_no_access_to_default_project_result_in_unscoped_token(self): - # create a disabled project to work with - self.create_new_default_project_for_user(self.user['id'], - self.domain_id) + def test_domain_scope_failed(self): + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + domain_id=self.domain['id']) + self.v3_create_token(auth_data, + expected_status=http_client.UNAUTHORIZED) - # attempt to authenticate without requesting a project + def test_auth_with_id(self): auth_data = self.build_authentication_request( user_id=self.user['id'], password=self.user['password']) - r = self.v3_authenticate_token(auth_data) + r = self.v3_create_token(auth_data) self.assertValidUnscopedTokenResponse(r) - def test_disabled_scope_project_domain_result_in_401(self): - # create a disabled domain - domain = self.new_domain_ref() - domain['enabled'] = False - self.resource_api.create_domain(domain['id'], domain) + token = r.headers.get('X-Subject-Token') - # create a project in the disabled domain - project = self.new_project_ref(domain_id=domain['id']) - self.resource_api.create_project(project['id'], project) + # test token auth + auth_data = self.build_authentication_request(token=token) + r = self.v3_create_token(auth_data) + self.assertValidUnscopedTokenResponse(r) - # assign some role to self.user for the project in the disabled domain - self.assignment_api.add_role_to_user_and_project( - self.user['id'], - project['id'], - self.role_id) + def get_v2_token(self, tenant_id=None): + body = { + 'auth': { + 'passwordCredentials': { + 'username': self.default_domain_user['name'], + 'password': self.default_domain_user['password'], + }, + }, + } + r = self.admin_request(method='POST', path='/v2.0/tokens', body=body) + return r - # user should not be able to auth with project_id + def test_validate_v2_unscoped_token_with_v3_api(self): + v2_token = self.get_v2_token().result['access']['token']['id'] + auth_data = self.build_authentication_request(token=v2_token) + r = self.v3_create_token(auth_data) + self.assertValidUnscopedTokenResponse(r) + + def test_validate_v2_scoped_token_with_v3_api(self): + v2_response = self.get_v2_token( + tenant_id=self.default_domain_project['id']) + result = v2_response.result + v2_token = result['access']['token']['id'] auth_data = self.build_authentication_request( - user_id=self.user['id'], - password=self.user['password'], - project_id=project['id']) - self.v3_authenticate_token(auth_data, - expected_status=http_client.UNAUTHORIZED) + token=v2_token, + project_id=self.default_domain_project['id']) + r = self.v3_create_token(auth_data) + self.assertValidScopedTokenResponse(r) - # user should not be able to auth with project_name & domain + def test_invalid_user_id(self): auth_data = self.build_authentication_request( - user_id=self.user['id'], - password=self.user['password'], - project_name=project['name'], - project_domain_id=domain['id']) - self.v3_authenticate_token(auth_data, - expected_status=http_client.UNAUTHORIZED) + user_id=uuid.uuid4().hex, + password=self.user['password']) + self.v3_create_token(auth_data, + expected_status=http_client.UNAUTHORIZED) - def test_auth_methods_with_different_identities_fails(self): - # get the token for a user. This is self.user which is different from - # self.default_domain_user. - token = self.get_scoped_token() - # try both password and token methods with different identities and it - # should fail + def test_invalid_user_name(self): auth_data = self.build_authentication_request( - token=token, - user_id=self.default_domain_user['id'], - password=self.default_domain_user['password']) - self.v3_authenticate_token(auth_data, - expected_status=http_client.UNAUTHORIZED) + username=uuid.uuid4().hex, + user_domain_id=self.domain['id'], + password=self.user['password']) + self.v3_create_token(auth_data, + expected_status=http_client.UNAUTHORIZED) + def test_invalid_domain_id(self): + auth_data = self.build_authentication_request( + username=self.user['name'], + user_domain_id=uuid.uuid4().hex, + password=self.user['password']) + self.v3_create_token(auth_data, + expected_status=http_client.UNAUTHORIZED) -class TestAuthJSONExternal(test_v3.RestfulTestCase): - content_type = 'json' + def test_invalid_domain_name(self): + auth_data = self.build_authentication_request( + username=self.user['name'], + user_domain_name=uuid.uuid4().hex, + password=self.user['password']) + self.v3_create_token(auth_data, + expected_status=http_client.UNAUTHORIZED) - def auth_plugin_config_override(self, methods=None, **method_classes): - self.config_fixture.config(group='auth', methods=[]) + def test_invalid_password(self): + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=uuid.uuid4().hex) + self.v3_create_token(auth_data, + expected_status=http_client.UNAUTHORIZED) - def test_remote_user_no_method(self): + def test_remote_user_no_realm(self): api = auth.controllers.Auth() context, auth_info, auth_context = self.build_external_auth_request( self.default_domain_user['name']) + api.authenticate(context, auth_info, auth_context) + self.assertEqual(self.default_domain_user['id'], + auth_context['user_id']) + # Now test to make sure the user name can, itself, contain the + # '@' character. + user = {'name': 'myname@mydivision'} + self.identity_api.update_user(self.default_domain_user['id'], user) + context, auth_info, auth_context = self.build_external_auth_request( + user["name"]) + api.authenticate(context, auth_info, auth_context) + self.assertEqual(self.default_domain_user['id'], + auth_context['user_id']) + + def test_remote_user_no_domain(self): + api = auth.controllers.Auth() + context, auth_info, auth_context = self.build_external_auth_request( + self.user['name']) self.assertRaises(exception.Unauthorized, api.authenticate, context, auth_info, auth_context) - -class TestTrustOptional(test_v3.RestfulTestCase): - def config_overrides(self): - super(TestTrustOptional, self).config_overrides() - self.config_fixture.config(group='trust', enabled=False) - - def test_trusts_404(self): - self.get('/OS-TRUST/trusts', body={'trust': {}}, - expected_status=http_client.NOT_FOUND) - self.post('/OS-TRUST/trusts', body={'trust': {}}, - expected_status=http_client.NOT_FOUND) - - def test_auth_with_scope_in_trust_forbidden(self): + def test_remote_user_and_password(self): + # both REMOTE_USER and password methods must pass. + # note that they do not have to match + api = auth.controllers.Auth() auth_data = self.build_authentication_request( - user_id=self.user['id'], - password=self.user['password'], - trust_id=uuid.uuid4().hex) - self.v3_authenticate_token(auth_data, - expected_status=http_client.FORBIDDEN) - - -class TestTrustRedelegation(test_v3.RestfulTestCase): - """Redelegation valid and secure - - Redelegation is a hierarchical structure of trusts between initial trustor - and a group of users allowed to impersonate trustor and act in his name. - Hierarchy is created in a process of trusting already trusted permissions - and organized as an adjacency list using 'redelegated_trust_id' field. - Redelegation is valid if each subsequent trust in a chain passes 'not more' - permissions than being redelegated. - - Trust constraints are: - * roles - set of roles trusted by trustor - * expiration_time - * allow_redelegation - a flag - * redelegation_count - decreasing value restricting length of trust chain - * remaining_uses - DISALLOWED when allow_redelegation == True - - Trust becomes invalid in case: - * trust roles were revoked from trustor - * one of the users in the delegation chain was disabled or deleted - * expiration time passed - * one of the parent trusts has become invalid - * one of the parent trusts was deleted - - """ + user_domain_id=self.default_domain_user['domain_id'], + username=self.default_domain_user['name'], + password=self.default_domain_user['password'])['auth'] + context, auth_info, auth_context = self.build_external_auth_request( + self.default_domain_user['name'], auth_data=auth_data) - def config_overrides(self): - super(TestTrustRedelegation, self).config_overrides() - self.config_fixture.config( - group='trust', - enabled=True, - allow_redelegation=True, - max_redelegation_count=10 - ) + api.authenticate(context, auth_info, auth_context) - def setUp(self): - super(TestTrustRedelegation, self).setUp() - # Create a trustee to delegate stuff to - trustee_user_ref = self.new_user_ref(domain_id=self.domain_id) - self.trustee_user = self.identity_api.create_user(trustee_user_ref) - self.trustee_user['password'] = trustee_user_ref['password'] + def test_remote_user_and_explicit_external(self): + # both REMOTE_USER and password methods must pass. + # note that they do not have to match + auth_data = self.build_authentication_request( + user_domain_id=self.domain['id'], + username=self.user['name'], + password=self.user['password'])['auth'] + auth_data['identity']['methods'] = ["password", "external"] + auth_data['identity']['external'] = {} + api = auth.controllers.Auth() + auth_info = auth.controllers.AuthInfo(None, auth_data) + auth_context = {'extras': {}, 'method_names': []} + self.assertRaises(exception.Unauthorized, + api.authenticate, + self.empty_context, + auth_info, + auth_context) - # trustor->trustee - self.redelegated_trust_ref = self.new_trust_ref( - trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user['id'], - project_id=self.project_id, - impersonation=True, - expires=dict(minutes=1), - role_ids=[self.role_id], - allow_redelegation=True) + def test_remote_user_bad_password(self): + # both REMOTE_USER and password methods must pass. + api = auth.controllers.Auth() + auth_data = self.build_authentication_request( + user_domain_id=self.domain['id'], + username=self.user['name'], + password='badpassword')['auth'] + context, auth_info, auth_context = self.build_external_auth_request( + self.default_domain_user['name'], auth_data=auth_data) + self.assertRaises(exception.Unauthorized, + api.authenticate, + context, + auth_info, + auth_context) - # trustor->trustee (no redelegation) - self.chained_trust_ref = self.new_trust_ref( - trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user['id'], - project_id=self.project_id, - impersonation=True, - role_ids=[self.role_id], - allow_redelegation=True) + def test_bind_not_set_with_remote_user(self): + self.config_fixture.config(group='token', bind=[]) + auth_data = self.build_authentication_request() + remote_user = self.default_domain_user['name'] + self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, + 'AUTH_TYPE': 'Negotiate'}) + r = self.v3_create_token(auth_data) + token = self.assertValidUnscopedTokenResponse(r) + self.assertNotIn('bind', token) - def _get_trust_token(self, trust): - trust_id = trust['id'] + # TODO(ayoung): move to TestPKITokenAPIs; it will be run for both formats + def test_verify_with_bound_token(self): + self.config_fixture.config(group='token', bind='kerberos') auth_data = self.build_authentication_request( - user_id=self.trustee_user['id'], - password=self.trustee_user['password'], - trust_id=trust_id) - trust_token = self.get_requested_token(auth_data) - return trust_token + project_id=self.project['id']) + remote_user = self.default_domain_user['name'] + self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, + 'AUTH_TYPE': 'Negotiate'}) - def test_depleted_redelegation_count_error(self): - self.redelegated_trust_ref['redelegation_count'] = 0 - r = self.post('/OS-TRUST/trusts', - body={'trust': self.redelegated_trust_ref}) - trust = self.assertValidTrustResponse(r) - trust_token = self._get_trust_token(trust) + token = self.get_requested_token(auth_data) + headers = {'X-Subject-Token': token} + r = self.get('/auth/tokens', headers=headers, token=token) + token = self.assertValidProjectScopedTokenResponse(r) + self.assertEqual(self.default_domain_user['name'], + token['bind']['kerberos']) - # Attempt to create a redelegated trust. - self.post('/OS-TRUST/trusts', - body={'trust': self.chained_trust_ref}, - token=trust_token, - expected_status=http_client.FORBIDDEN) + def test_auth_with_bind_token(self): + self.config_fixture.config(group='token', bind=['kerberos']) - def test_modified_redelegation_count_error(self): - r = self.post('/OS-TRUST/trusts', - body={'trust': self.redelegated_trust_ref}) - trust = self.assertValidTrustResponse(r) - trust_token = self._get_trust_token(trust) + auth_data = self.build_authentication_request() + remote_user = self.default_domain_user['name'] + self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, + 'AUTH_TYPE': 'Negotiate'}) + r = self.v3_create_token(auth_data) - # Attempt to create a redelegated trust with incorrect - # redelegation_count. - correct = trust['redelegation_count'] - 1 - incorrect = correct - 1 - self.chained_trust_ref['redelegation_count'] = incorrect - self.post('/OS-TRUST/trusts', - body={'trust': self.chained_trust_ref}, - token=trust_token, - expected_status=http_client.FORBIDDEN) + # the unscoped token should have bind information in it + token = self.assertValidUnscopedTokenResponse(r) + self.assertEqual(remote_user, token['bind']['kerberos']) - def test_max_redelegation_count_constraint(self): - incorrect = CONF.trust.max_redelegation_count + 1 - self.redelegated_trust_ref['redelegation_count'] = incorrect - self.post('/OS-TRUST/trusts', - body={'trust': self.redelegated_trust_ref}, - expected_status=http_client.FORBIDDEN) + token = r.headers.get('X-Subject-Token') - def test_redelegation_expiry(self): - r = self.post('/OS-TRUST/trusts', - body={'trust': self.redelegated_trust_ref}) - trust = self.assertValidTrustResponse(r) - trust_token = self._get_trust_token(trust) + # using unscoped token with remote user succeeds + auth_params = {'token': token, 'project_id': self.project_id} + auth_data = self.build_authentication_request(**auth_params) + r = self.v3_create_token(auth_data) + token = self.assertValidProjectScopedTokenResponse(r) - # Attempt to create a redelegated trust supposed to last longer - # than the parent trust: let's give it 10 minutes (>1 minute). - too_long_live_chained_trust_ref = self.new_trust_ref( - trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user['id'], - project_id=self.project_id, - impersonation=True, - expires=dict(minutes=10), - role_ids=[self.role_id]) - self.post('/OS-TRUST/trusts', - body={'trust': too_long_live_chained_trust_ref}, - token=trust_token, - expected_status=http_client.FORBIDDEN) + # the bind information should be carried over from the original token + self.assertEqual(remote_user, token['bind']['kerberos']) - def test_redelegation_remaining_uses(self): - r = self.post('/OS-TRUST/trusts', - body={'trust': self.redelegated_trust_ref}) - trust = self.assertValidTrustResponse(r) - trust_token = self._get_trust_token(trust) + def test_v2_v3_bind_token_intermix(self): + self.config_fixture.config(group='token', bind='kerberos') + + # we need our own user registered to the default domain because of + # the way external auth works. + remote_user = self.default_domain_user['name'] + self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, + 'AUTH_TYPE': 'Negotiate'}) + body = {'auth': {}} + resp = self.admin_request(path='/v2.0/tokens', + method='POST', + body=body) - # Attempt to create a redelegated trust with remaining_uses defined. - # It must fail according to specification: remaining_uses must be - # omitted for trust redelegation. Any number here. - self.chained_trust_ref['remaining_uses'] = 5 - self.post('/OS-TRUST/trusts', - body={'trust': self.chained_trust_ref}, - token=trust_token, - expected_status=http_client.BAD_REQUEST) + v2_token_data = resp.result - def test_roles_subset(self): - # Build second role - role = self.new_role_ref() - self.role_api.create_role(role['id'], role) - # assign a new role to the user - self.assignment_api.create_grant(role_id=role['id'], - user_id=self.user_id, - project_id=self.project_id) + bind = v2_token_data['access']['token']['bind'] + self.assertEqual(self.default_domain_user['name'], bind['kerberos']) - # Create first trust with extended set of roles - ref = self.redelegated_trust_ref - ref['roles'].append({'id': role['id']}) - r = self.post('/OS-TRUST/trusts', - body={'trust': ref}) - trust = self.assertValidTrustResponse(r) - # Trust created with exact set of roles (checked by role id) - role_id_set = set(r['id'] for r in ref['roles']) - trust_role_id_set = set(r['id'] for r in trust['roles']) - self.assertEqual(role_id_set, trust_role_id_set) + v2_token_id = v2_token_data['access']['token']['id'] + # NOTE(gyee): self.get() will try to obtain an auth token if one + # is not provided. When REMOTE_USER is present in the request + # environment, the external user auth plugin is used in conjunction + # with the password auth for the admin user. Therefore, we need to + # cleanup the REMOTE_USER information from the previous call. + del self.admin_app.extra_environ['REMOTE_USER'] + headers = {'X-Subject-Token': v2_token_id} + resp = self.get('/auth/tokens', headers=headers) + token_data = resp.result - trust_token = self._get_trust_token(trust) + self.assertDictEqual(v2_token_data['access']['token']['bind'], + token_data['token']['bind']) - # Chain second trust with roles subset - r = self.post('/OS-TRUST/trusts', - body={'trust': self.chained_trust_ref}, - token=trust_token) - trust2 = self.assertValidTrustResponse(r) - # First trust contains roles superset - # Second trust contains roles subset - role_id_set1 = set(r['id'] for r in trust['roles']) - role_id_set2 = set(r['id'] for r in trust2['roles']) - self.assertThat(role_id_set1, matchers.GreaterThan(role_id_set2)) + def test_authenticating_a_user_with_no_password(self): + user = unit.new_user_ref(domain_id=self.domain['id']) + del user['password'] # can't have a password for this test + user = self.identity_api.create_user(user) - def test_redelegate_with_role_by_name(self): - # For role by name testing - ref = self.new_trust_ref( - trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user['id'], - project_id=self.project_id, - impersonation=True, - expires=dict(minutes=1), - role_names=[self.role['name']], - allow_redelegation=True) - r = self.post('/OS-TRUST/trusts', - body={'trust': ref}) - trust = self.assertValidTrustResponse(r) - # Ensure we can get a token with this trust - trust_token = self._get_trust_token(trust) - # Chain second trust with roles subset - ref = self.new_trust_ref( - trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user['id'], - project_id=self.project_id, - impersonation=True, - role_names=[self.role['name']], - allow_redelegation=True) - r = self.post('/OS-TRUST/trusts', - body={'trust': ref}, - token=trust_token) - trust = self.assertValidTrustResponse(r) - # Ensure we can get a token with this trust - self._get_trust_token(trust) + auth_data = self.build_authentication_request( + user_id=user['id'], + password='password') - def test_redelegate_new_role_fails(self): - r = self.post('/OS-TRUST/trusts', - body={'trust': self.redelegated_trust_ref}) - trust = self.assertValidTrustResponse(r) - trust_token = self._get_trust_token(trust) + self.v3_create_token(auth_data, + expected_status=http_client.UNAUTHORIZED) - # Build second trust with a role not in parent's roles - role = self.new_role_ref() - self.role_api.create_role(role['id'], role) - # assign a new role to the user - self.assignment_api.create_grant(role_id=role['id'], - user_id=self.user_id, - project_id=self.project_id) + def test_disabled_default_project_result_in_unscoped_token(self): + # create a disabled project to work with + project = self.create_new_default_project_for_user( + self.user['id'], self.domain_id, enable_project=False) - # Try to chain a trust with the role not from parent trust - self.chained_trust_ref['roles'] = [{'id': role['id']}] + # assign a role to user for the new project + self.assignment_api.add_role_to_user_and_project(self.user['id'], + project['id'], + self.role_id) - # Bypass policy enforcement - with mock.patch.object(rules, 'enforce', return_value=True): - self.post('/OS-TRUST/trusts', - body={'trust': self.chained_trust_ref}, - token=trust_token, - expected_status=http_client.FORBIDDEN) + # attempt to authenticate without requesting a project + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password']) + r = self.v3_create_token(auth_data) + self.assertValidUnscopedTokenResponse(r) - def test_redelegation_terminator(self): - r = self.post('/OS-TRUST/trusts', - body={'trust': self.redelegated_trust_ref}) - trust = self.assertValidTrustResponse(r) - trust_token = self._get_trust_token(trust) + def test_disabled_default_project_domain_result_in_unscoped_token(self): + domain_ref = unit.new_domain_ref() + r = self.post('/domains', body={'domain': domain_ref}) + domain = self.assertValidDomainResponse(r, domain_ref) - # Build second trust - the terminator - ref = dict(self.chained_trust_ref, - redelegation_count=1, - allow_redelegation=False) + project = self.create_new_default_project_for_user( + self.user['id'], domain['id']) - r = self.post('/OS-TRUST/trusts', - body={'trust': ref}, - token=trust_token) + # assign a role to user for the new project + self.assignment_api.add_role_to_user_and_project(self.user['id'], + project['id'], + self.role_id) - trust = self.assertValidTrustResponse(r) - # Check that allow_redelegation == False caused redelegation_count - # to be set to 0, while allow_redelegation is removed - self.assertNotIn('allow_redelegation', trust) - self.assertEqual(0, trust['redelegation_count']) - trust_token = self._get_trust_token(trust) + # now disable the project domain + body = {'domain': {'enabled': False}} + r = self.patch('/domains/%(domain_id)s' % {'domain_id': domain['id']}, + body=body) + self.assertValidDomainResponse(r) - # Build third trust, same as second - self.post('/OS-TRUST/trusts', - body={'trust': ref}, - token=trust_token, - expected_status=http_client.FORBIDDEN) + # attempt to authenticate without requesting a project + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password']) + r = self.v3_create_token(auth_data) + self.assertValidUnscopedTokenResponse(r) + def test_no_access_to_default_project_result_in_unscoped_token(self): + # create a disabled project to work with + self.create_new_default_project_for_user(self.user['id'], + self.domain_id) -class TestTrustChain(test_v3.RestfulTestCase): + # attempt to authenticate without requesting a project + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password']) + r = self.v3_create_token(auth_data) + self.assertValidUnscopedTokenResponse(r) - def config_overrides(self): - super(TestTrustChain, self).config_overrides() - self.config_fixture.config( - group='trust', - enabled=True, - allow_redelegation=True, - max_redelegation_count=10 - ) + def test_disabled_scope_project_domain_result_in_401(self): + # create a disabled domain + domain = unit.new_domain_ref() + domain = self.resource_api.create_domain(domain['id'], domain) - def setUp(self): - super(TestTrustChain, self).setUp() - # Create trust chain - self.user_chain = list() - self.trust_chain = list() - for _ in range(3): - user_ref = self.new_user_ref(domain_id=self.domain_id) - user = self.identity_api.create_user(user_ref) - user['password'] = user_ref['password'] - self.user_chain.append(user) + # create a project in the domain + project = unit.new_project_ref(domain_id=domain['id']) + self.resource_api.create_project(project['id'], project) - # trustor->trustee - trustee = self.user_chain[0] - trust_ref = self.new_trust_ref( - trustor_user_id=self.user_id, - trustee_user_id=trustee['id'], - project_id=self.project_id, - impersonation=True, - expires=dict(minutes=1), - role_ids=[self.role_id]) - trust_ref.update( - allow_redelegation=True, - redelegation_count=3) + # assign some role to self.user for the project in the domain + self.assignment_api.add_role_to_user_and_project( + self.user['id'], + project['id'], + self.role_id) - r = self.post('/OS-TRUST/trusts', - body={'trust': trust_ref}) + # Disable the domain + domain['enabled'] = False + self.resource_api.update_domain(domain['id'], domain) - trust = self.assertValidTrustResponse(r) + # user should not be able to auth with project_id auth_data = self.build_authentication_request( - user_id=trustee['id'], - password=trustee['password'], - trust_id=trust['id']) - trust_token = self.get_requested_token(auth_data) - self.trust_chain.append(trust) + user_id=self.user['id'], + password=self.user['password'], + project_id=project['id']) + self.v3_create_token(auth_data, + expected_status=http_client.UNAUTHORIZED) - for trustee in self.user_chain[1:]: - trust_ref = self.new_trust_ref( - trustor_user_id=self.user_id, - trustee_user_id=trustee['id'], - project_id=self.project_id, - impersonation=True, - role_ids=[self.role_id]) - trust_ref.update( - allow_redelegation=True) - r = self.post('/OS-TRUST/trusts', - body={'trust': trust_ref}, - token=trust_token) - trust = self.assertValidTrustResponse(r) - auth_data = self.build_authentication_request( - user_id=trustee['id'], - password=trustee['password'], - trust_id=trust['id']) - trust_token = self.get_requested_token(auth_data) - self.trust_chain.append(trust) + # user should not be able to auth with project_name & domain + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_name=project['name'], + project_domain_id=domain['id']) + self.v3_create_token(auth_data, + expected_status=http_client.UNAUTHORIZED) - trustee = self.user_chain[-1] - trust = self.trust_chain[-1] + def test_auth_methods_with_different_identities_fails(self): + # get the token for a user. This is self.user which is different from + # self.default_domain_user. + token = self.get_scoped_token() + # try both password and token methods with different identities and it + # should fail auth_data = self.build_authentication_request( - user_id=trustee['id'], - password=trustee['password'], - trust_id=trust['id']) + token=token, + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password']) + self.v3_create_token(auth_data, + expected_status=http_client.UNAUTHORIZED) + + def test_authenticate_fails_if_project_unsafe(self): + """Verify authenticate to a project with unsafe name fails.""" + # Start with url name restrictions off, so we can create the unsafe + # named project + self.config_fixture.config(group='resource', + project_name_url_safe='off') + unsafe_name = 'i am not / safe' + project = unit.new_project_ref(domain_id=test_v3.DEFAULT_DOMAIN_ID, + name=unsafe_name) + self.resource_api.create_project(project['id'], project) + role_member = unit.new_role_ref() + self.role_api.create_role(role_member['id'], role_member) + self.assignment_api.add_role_to_user_and_project( + self.user['id'], project['id'], role_member['id']) - self.last_token = self.get_requested_token(auth_data) + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_name=project['name'], + project_domain_id=test_v3.DEFAULT_DOMAIN_ID) + + # Since name url restriction is off, we should be able to autenticate + self.v3_create_token(auth_data) + + # Set the name url restriction to new, which should still allow us to + # authenticate + self.config_fixture.config(group='resource', + project_name_url_safe='new') + self.v3_create_token(auth_data) + + # Set the name url restriction to strict and we should fail to + # authenticate + self.config_fixture.config(group='resource', + project_name_url_safe='strict') + self.v3_create_token(auth_data, + expected_status=http_client.UNAUTHORIZED) + + def test_authenticate_fails_if_domain_unsafe(self): + """Verify authenticate to a domain with unsafe name fails.""" + # Start with url name restrictions off, so we can create the unsafe + # named domain + self.config_fixture.config(group='resource', + domain_name_url_safe='off') + unsafe_name = 'i am not / safe' + domain = unit.new_domain_ref(name=unsafe_name) + self.resource_api.create_domain(domain['id'], domain) + role_member = unit.new_role_ref() + self.role_api.create_role(role_member['id'], role_member) + self.assignment_api.create_grant( + role_member['id'], + user_id=self.user['id'], + domain_id=domain['id']) - def assert_user_authenticate(self, user): auth_data = self.build_authentication_request( - user_id=user['id'], - password=user['password'] - ) - r = self.v3_authenticate_token(auth_data) - self.assertValidTokenResponse(r) + user_id=self.user['id'], + password=self.user['password'], + domain_name=domain['name']) + + # Since name url restriction is off, we should be able to autenticate + self.v3_create_token(auth_data) + + # Set the name url restriction to new, which should still allow us to + # authenticate + self.config_fixture.config(group='resource', + project_name_url_safe='new') + self.v3_create_token(auth_data) + + # Set the name url restriction to strict and we should fail to + # authenticate + self.config_fixture.config(group='resource', + domain_name_url_safe='strict') + self.v3_create_token(auth_data, + expected_status=http_client.UNAUTHORIZED) + + def test_authenticate_fails_to_project_if_domain_unsafe(self): + """Verify authenticate to a project using unsafe domain name fails.""" + # Start with url name restrictions off, so we can create the unsafe + # named domain + self.config_fixture.config(group='resource', + domain_name_url_safe='off') + unsafe_name = 'i am not / safe' + domain = unit.new_domain_ref(name=unsafe_name) + self.resource_api.create_domain(domain['id'], domain) + # Add a (safely named) project to that domain + project = unit.new_project_ref(domain_id=domain['id']) + self.resource_api.create_project(project['id'], project) + role_member = unit.new_role_ref() + self.role_api.create_role(role_member['id'], role_member) + self.assignment_api.create_grant( + role_member['id'], + user_id=self.user['id'], + project_id=project['id']) - def assert_trust_tokens_revoked(self, trust_id): - trustee = self.user_chain[0] + # An auth request via project ID, but specifying domain by name auth_data = self.build_authentication_request( - user_id=trustee['id'], - password=trustee['password'] - ) - r = self.v3_authenticate_token(auth_data) - self.assertValidTokenResponse(r) + user_id=self.user['id'], + password=self.user['password'], + project_name=project['name'], + project_domain_name=domain['name']) - revocation_response = self.get('/OS-REVOKE/events') - revocation_events = revocation_response.json_body['events'] - found = False - for event in revocation_events: - if event.get('OS-TRUST:trust_id') == trust_id: - found = True - self.assertTrue(found, 'event with trust_id %s not found in list' % - trust_id) + # Since name url restriction is off, we should be able to autenticate + self.v3_create_token(auth_data) - def test_delete_trust_cascade(self): - self.assert_user_authenticate(self.user_chain[0]) - self.delete('/OS-TRUST/trusts/%(trust_id)s' % { - 'trust_id': self.trust_chain[0]['id']}, - expected_status=204) + # Set the name url restriction to new, which should still allow us to + # authenticate + self.config_fixture.config(group='resource', + project_name_url_safe='new') + self.v3_create_token(auth_data) - headers = {'X-Subject-Token': self.last_token} - self.head('/auth/tokens', headers=headers, - expected_status=http_client.NOT_FOUND) - self.assert_trust_tokens_revoked(self.trust_chain[0]['id']) + # Set the name url restriction to strict and we should fail to + # authenticate + self.config_fixture.config(group='resource', + domain_name_url_safe='strict') + self.v3_create_token(auth_data, + expected_status=http_client.UNAUTHORIZED) - def test_delete_broken_chain(self): - self.assert_user_authenticate(self.user_chain[0]) - self.delete('/OS-TRUST/trusts/%(trust_id)s' % { - 'trust_id': self.trust_chain[1]['id']}, - expected_status=204) - self.delete('/OS-TRUST/trusts/%(trust_id)s' % { - 'trust_id': self.trust_chain[0]['id']}, - expected_status=204) +class TestAuthJSONExternal(test_v3.RestfulTestCase): + content_type = 'json' - def test_trustor_roles_revoked(self): - self.assert_user_authenticate(self.user_chain[0]) + def auth_plugin_config_override(self, methods=None, **method_classes): + self.config_fixture.config(group='auth', methods=[]) - self.assignment_api.remove_role_from_user_and_project( - self.user_id, self.project_id, self.role_id - ) + def test_remote_user_no_method(self): + api = auth.controllers.Auth() + context, auth_info, auth_context = self.build_external_auth_request( + self.default_domain_user['name']) + self.assertRaises(exception.Unauthorized, + api.authenticate, + context, + auth_info, + auth_context) - auth_data = self.build_authentication_request( - token=self.last_token, - trust_id=self.trust_chain[-1]['id']) - self.v3_authenticate_token(auth_data, - expected_status=http_client.NOT_FOUND) - def test_intermediate_user_disabled(self): - self.assert_user_authenticate(self.user_chain[0]) +class TestTrustOptional(test_v3.RestfulTestCase): + def config_overrides(self): + super(TestTrustOptional, self).config_overrides() + self.config_fixture.config(group='trust', enabled=False) - disabled = self.user_chain[0] - disabled['enabled'] = False - self.identity_api.update_user(disabled['id'], disabled) + def test_trusts_returns_not_found(self): + self.get('/OS-TRUST/trusts', body={'trust': {}}, + expected_status=http_client.NOT_FOUND) + self.post('/OS-TRUST/trusts', body={'trust': {}}, + expected_status=http_client.NOT_FOUND) - # Bypass policy enforcement - with mock.patch.object(rules, 'enforce', return_value=True): - headers = {'X-Subject-Token': self.last_token} - self.head('/auth/tokens', headers=headers, - expected_status=http_client.FORBIDDEN) + def test_auth_with_scope_in_trust_forbidden(self): + auth_data = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + trust_id=uuid.uuid4().hex) + self.v3_create_token(auth_data, + expected_status=http_client.FORBIDDEN) - def test_intermediate_user_deleted(self): - self.assert_user_authenticate(self.user_chain[0]) - self.identity_api.delete_user(self.user_chain[0]['id']) +class TrustAPIBehavior(test_v3.RestfulTestCase): + """Redelegation valid and secure - # Bypass policy enforcement - with mock.patch.object(rules, 'enforce', return_value=True): - headers = {'X-Subject-Token': self.last_token} - self.head('/auth/tokens', headers=headers, - expected_status=http_client.FORBIDDEN) + Redelegation is a hierarchical structure of trusts between initial trustor + and a group of users allowed to impersonate trustor and act in his name. + Hierarchy is created in a process of trusting already trusted permissions + and organized as an adjacency list using 'redelegated_trust_id' field. + Redelegation is valid if each subsequent trust in a chain passes 'not more' + permissions than being redelegated. + + Trust constraints are: + * roles - set of roles trusted by trustor + * expiration_time + * allow_redelegation - a flag + * redelegation_count - decreasing value restricting length of trust chain + * remaining_uses - DISALLOWED when allow_redelegation == True + Trust becomes invalid in case: + * trust roles were revoked from trustor + * one of the users in the delegation chain was disabled or deleted + * expiration time passed + * one of the parent trusts has become invalid + * one of the parent trusts was deleted -class TestTrustAuth(test_v3.RestfulTestCase): - EXTENSION_NAME = 'revoke' - EXTENSION_TO_ADD = 'revoke_extension' + """ def config_overrides(self): - super(TestTrustAuth, self).config_overrides() - self.config_fixture.config(group='revoke', driver='kvs') + super(TrustAPIBehavior, self).config_overrides() self.config_fixture.config( - group='token', - provider='pki', - revoke_by_id=False) - self.config_fixture.config(group='trust', enabled=True) + group='trust', + enabled=True, + allow_redelegation=True, + max_redelegation_count=10 + ) def setUp(self): - super(TestTrustAuth, self).setUp() - - # create a trustee to delegate stuff to - self.trustee_user = self.new_user_ref(domain_id=self.domain_id) - password = self.trustee_user['password'] - self.trustee_user = self.identity_api.create_user(self.trustee_user) - self.trustee_user['password'] = password - self.trustee_user_id = self.trustee_user['id'] - - def test_create_trust_bad_request(self): - # The server returns a 403 Forbidden rather than a 400, see bug 1133435 - self.post('/OS-TRUST/trusts', body={'trust': {}}, - expected_status=http_client.FORBIDDEN) - - def test_create_unscoped_trust(self): - ref = self.new_trust_ref( - trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user_id) - r = self.post('/OS-TRUST/trusts', body={'trust': ref}) - self.assertValidTrustResponse(r, ref) + super(TrustAPIBehavior, self).setUp() + # Create a trustee to delegate stuff to + self.trustee_user = unit.create_user(self.identity_api, + domain_id=self.domain_id) - def test_create_trust_no_roles(self): - ref = self.new_trust_ref( + # trustor->trustee + self.redelegated_trust_ref = unit.new_trust_ref( trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user_id, - project_id=self.project_id) - self.post('/OS-TRUST/trusts', body={'trust': ref}, - expected_status=http_client.FORBIDDEN) + trustee_user_id=self.trustee_user['id'], + project_id=self.project_id, + impersonation=True, + expires=dict(minutes=1), + role_ids=[self.role_id], + allow_redelegation=True) - def _initialize_test_consume_trust(self, count): - # Make sure remaining_uses is decremented as we consume the trust - ref = self.new_trust_ref( + # trustor->trustee (no redelegation) + self.chained_trust_ref = unit.new_trust_ref( trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user_id, + trustee_user_id=self.trustee_user['id'], project_id=self.project_id, - remaining_uses=count, - role_ids=[self.role_id]) - r = self.post('/OS-TRUST/trusts', body={'trust': ref}) - # make sure the trust exists - trust = self.assertValidTrustResponse(r, ref) - r = self.get( - '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}, - expected_status=200) - # get a token for the trustee - auth_data = self.build_authentication_request( - user_id=self.trustee_user['id'], - password=self.trustee_user['password']) - r = self.v3_authenticate_token(auth_data) - token = r.headers.get('X-Subject-Token') - # get a trust token, consume one use - auth_data = self.build_authentication_request( - token=token, - trust_id=trust['id']) - r = self.v3_authenticate_token(auth_data) - return trust - - def test_consume_trust_once(self): - trust = self._initialize_test_consume_trust(2) - # check decremented value - r = self.get( - '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}, - expected_status=200) - trust = r.result.get('trust') - self.assertIsNotNone(trust) - self.assertEqual(1, trust['remaining_uses']) + impersonation=True, + role_ids=[self.role_id], + allow_redelegation=True) - def test_create_one_time_use_trust(self): - trust = self._initialize_test_consume_trust(1) - # No more uses, the trust is made unavailable - self.get( - '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}, - expected_status=http_client.NOT_FOUND) - # this time we can't get a trust token + def _get_trust_token(self, trust): + trust_id = trust['id'] auth_data = self.build_authentication_request( user_id=self.trustee_user['id'], password=self.trustee_user['password'], - trust_id=trust['id']) - self.v3_authenticate_token(auth_data, - expected_status=http_client.UNAUTHORIZED) - - def test_create_trust_with_bad_values_for_remaining_uses(self): - # negative values for the remaining_uses parameter are forbidden - self._create_trust_with_bad_remaining_use(bad_value=-1) - # 0 is a forbidden value as well - self._create_trust_with_bad_remaining_use(bad_value=0) - # as are non integer values - self._create_trust_with_bad_remaining_use(bad_value="a bad value") - self._create_trust_with_bad_remaining_use(bad_value=7.2) - - def _create_trust_with_bad_remaining_use(self, bad_value): - ref = self.new_trust_ref( - trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user_id, - project_id=self.project_id, - remaining_uses=bad_value, - role_ids=[self.role_id]) - self.post('/OS-TRUST/trusts', - body={'trust': ref}, - expected_status=http_client.BAD_REQUEST) - - def test_invalid_trust_request_without_impersonation(self): - ref = self.new_trust_ref( - trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user_id, - project_id=self.project_id, - role_ids=[self.role_id]) + trust_id=trust_id) + trust_token = self.get_requested_token(auth_data) + return trust_token - del ref['impersonation'] + def test_depleted_redelegation_count_error(self): + self.redelegated_trust_ref['redelegation_count'] = 0 + r = self.post('/OS-TRUST/trusts', + body={'trust': self.redelegated_trust_ref}) + trust = self.assertValidTrustResponse(r) + trust_token = self._get_trust_token(trust) + # Attempt to create a redelegated trust. self.post('/OS-TRUST/trusts', - body={'trust': ref}, - expected_status=http_client.BAD_REQUEST) - - def test_invalid_trust_request_without_trustee(self): - ref = self.new_trust_ref( - trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user_id, - project_id=self.project_id, - role_ids=[self.role_id]) + body={'trust': self.chained_trust_ref}, + token=trust_token, + expected_status=http_client.FORBIDDEN) - del ref['trustee_user_id'] + def test_modified_redelegation_count_error(self): + r = self.post('/OS-TRUST/trusts', + body={'trust': self.redelegated_trust_ref}) + trust = self.assertValidTrustResponse(r) + trust_token = self._get_trust_token(trust) + # Attempt to create a redelegated trust with incorrect + # redelegation_count. + correct = trust['redelegation_count'] - 1 + incorrect = correct - 1 + self.chained_trust_ref['redelegation_count'] = incorrect self.post('/OS-TRUST/trusts', - body={'trust': ref}, - expected_status=http_client.BAD_REQUEST) + body={'trust': self.chained_trust_ref}, + token=trust_token, + expected_status=http_client.FORBIDDEN) - def test_create_unlimited_use_trust(self): - # by default trusts are unlimited in terms of tokens that can be - # generated from them, this test creates such a trust explicitly - ref = self.new_trust_ref( - trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user_id, - project_id=self.project_id, - remaining_uses=None, - role_ids=[self.role_id]) - r = self.post('/OS-TRUST/trusts', body={'trust': ref}) - trust = self.assertValidTrustResponse(r, ref) + def test_max_redelegation_count_constraint(self): + incorrect = CONF.trust.max_redelegation_count + 1 + self.redelegated_trust_ref['redelegation_count'] = incorrect + self.post('/OS-TRUST/trusts', + body={'trust': self.redelegated_trust_ref}, + expected_status=http_client.FORBIDDEN) - r = self.get( - '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}, - expected_status=200) - auth_data = self.build_authentication_request( - user_id=self.trustee_user['id'], - password=self.trustee_user['password']) - r = self.v3_authenticate_token(auth_data) - token = r.headers.get('X-Subject-Token') - auth_data = self.build_authentication_request( - token=token, - trust_id=trust['id']) - r = self.v3_authenticate_token(auth_data) - r = self.get( - '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}, - expected_status=200) - trust = r.result.get('trust') - self.assertIsNone(trust['remaining_uses']) + def test_redelegation_expiry(self): + r = self.post('/OS-TRUST/trusts', + body={'trust': self.redelegated_trust_ref}) + trust = self.assertValidTrustResponse(r) + trust_token = self._get_trust_token(trust) - def test_trust_crud(self): - ref = self.new_trust_ref( + # Attempt to create a redelegated trust supposed to last longer + # than the parent trust: let's give it 10 minutes (>1 minute). + too_long_live_chained_trust_ref = unit.new_trust_ref( trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user_id, + trustee_user_id=self.trustee_user['id'], project_id=self.project_id, + impersonation=True, + expires=dict(minutes=10), role_ids=[self.role_id]) - r = self.post('/OS-TRUST/trusts', body={'trust': ref}) - trust = self.assertValidTrustResponse(r, ref) + self.post('/OS-TRUST/trusts', + body={'trust': too_long_live_chained_trust_ref}, + token=trust_token, + expected_status=http_client.FORBIDDEN) - r = self.get( - '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}, - expected_status=200) - self.assertValidTrustResponse(r, ref) + def test_redelegation_remaining_uses(self): + r = self.post('/OS-TRUST/trusts', + body={'trust': self.redelegated_trust_ref}) + trust = self.assertValidTrustResponse(r) + trust_token = self._get_trust_token(trust) - # validate roles on the trust - r = self.get( - '/OS-TRUST/trusts/%(trust_id)s/roles' % { - 'trust_id': trust['id']}, - expected_status=200) - roles = self.assertValidRoleListResponse(r, self.role) - self.assertIn(self.role['id'], [x['id'] for x in roles]) - self.head( - '/OS-TRUST/trusts/%(trust_id)s/roles/%(role_id)s' % { - 'trust_id': trust['id'], - 'role_id': self.role['id']}, - expected_status=200) - r = self.get( - '/OS-TRUST/trusts/%(trust_id)s/roles/%(role_id)s' % { - 'trust_id': trust['id'], - 'role_id': self.role['id']}, - expected_status=200) - self.assertValidRoleResponse(r, self.role) + # Attempt to create a redelegated trust with remaining_uses defined. + # It must fail according to specification: remaining_uses must be + # omitted for trust redelegation. Any number here. + self.chained_trust_ref['remaining_uses'] = 5 + self.post('/OS-TRUST/trusts', + body={'trust': self.chained_trust_ref}, + token=trust_token, + expected_status=http_client.BAD_REQUEST) - r = self.get('/OS-TRUST/trusts', expected_status=200) - self.assertValidTrustListResponse(r, trust) + def test_roles_subset(self): + # Build second role + role = unit.new_role_ref() + self.role_api.create_role(role['id'], role) + # assign a new role to the user + self.assignment_api.create_grant(role_id=role['id'], + user_id=self.user_id, + project_id=self.project_id) - # trusts are immutable - self.patch( - '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}, - body={'trust': ref}, - expected_status=http_client.NOT_FOUND) + # Create first trust with extended set of roles + ref = self.redelegated_trust_ref + ref['expires_at'] = datetime.datetime.utcnow().replace( + year=2032).strftime(unit.TIME_FORMAT) + ref['roles'].append({'id': role['id']}) + r = self.post('/OS-TRUST/trusts', + body={'trust': ref}) + trust = self.assertValidTrustResponse(r) + # Trust created with exact set of roles (checked by role id) + role_id_set = set(r['id'] for r in ref['roles']) + trust_role_id_set = set(r['id'] for r in trust['roles']) + self.assertEqual(role_id_set, trust_role_id_set) - self.delete( - '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}, - expected_status=204) + trust_token = self._get_trust_token(trust) - self.get( - '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}, - expected_status=http_client.NOT_FOUND) + # Chain second trust with roles subset + self.chained_trust_ref['expires_at'] = ( + datetime.datetime.utcnow().replace(year=2028).strftime( + unit.TIME_FORMAT)) + r = self.post('/OS-TRUST/trusts', + body={'trust': self.chained_trust_ref}, + token=trust_token) + trust2 = self.assertValidTrustResponse(r) + # First trust contains roles superset + # Second trust contains roles subset + role_id_set1 = set(r['id'] for r in trust['roles']) + role_id_set2 = set(r['id'] for r in trust2['roles']) + self.assertThat(role_id_set1, matchers.GreaterThan(role_id_set2)) - def test_create_trust_trustee_404(self): - ref = self.new_trust_ref( + def test_redelegate_with_role_by_name(self): + # For role by name testing + ref = unit.new_trust_ref( trustor_user_id=self.user_id, - trustee_user_id=uuid.uuid4().hex, + trustee_user_id=self.trustee_user['id'], project_id=self.project_id, - role_ids=[self.role_id]) - self.post('/OS-TRUST/trusts', body={'trust': ref}, - expected_status=http_client.NOT_FOUND) - - def test_create_trust_trustor_trustee_backwards(self): - ref = self.new_trust_ref( - trustor_user_id=self.trustee_user_id, - trustee_user_id=self.user_id, + impersonation=True, + expires=dict(minutes=1), + role_names=[self.role['name']], + allow_redelegation=True) + ref['expires_at'] = datetime.datetime.utcnow().replace( + year=2032).strftime(unit.TIME_FORMAT) + r = self.post('/OS-TRUST/trusts', + body={'trust': ref}) + trust = self.assertValidTrustResponse(r) + # Ensure we can get a token with this trust + trust_token = self._get_trust_token(trust) + # Chain second trust with roles subset + ref = unit.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user['id'], project_id=self.project_id, - role_ids=[self.role_id]) - self.post('/OS-TRUST/trusts', body={'trust': ref}, - expected_status=http_client.FORBIDDEN) + impersonation=True, + role_names=[self.role['name']], + allow_redelegation=True) + ref['expires_at'] = datetime.datetime.utcnow().replace( + year=2028).strftime(unit.TIME_FORMAT) + r = self.post('/OS-TRUST/trusts', + body={'trust': ref}, + token=trust_token) + trust = self.assertValidTrustResponse(r) + # Ensure we can get a token with this trust + self._get_trust_token(trust) - def test_create_trust_project_404(self): - ref = self.new_trust_ref( - trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user_id, - project_id=uuid.uuid4().hex, - role_ids=[self.role_id]) - self.post('/OS-TRUST/trusts', body={'trust': ref}, - expected_status=http_client.NOT_FOUND) + def test_redelegate_new_role_fails(self): + r = self.post('/OS-TRUST/trusts', + body={'trust': self.redelegated_trust_ref}) + trust = self.assertValidTrustResponse(r) + trust_token = self._get_trust_token(trust) - def test_create_trust_role_id_404(self): - ref = self.new_trust_ref( - trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user_id, - project_id=self.project_id, - role_ids=[uuid.uuid4().hex]) - self.post('/OS-TRUST/trusts', body={'trust': ref}, - expected_status=http_client.NOT_FOUND) + # Build second trust with a role not in parent's roles + role = unit.new_role_ref() + self.role_api.create_role(role['id'], role) + # assign a new role to the user + self.assignment_api.create_grant(role_id=role['id'], + user_id=self.user_id, + project_id=self.project_id) - def test_create_trust_role_name_404(self): - ref = self.new_trust_ref( - trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user_id, - project_id=self.project_id, - role_names=[uuid.uuid4().hex]) - self.post('/OS-TRUST/trusts', body={'trust': ref}, - expected_status=http_client.NOT_FOUND) + # Try to chain a trust with the role not from parent trust + self.chained_trust_ref['roles'] = [{'id': role['id']}] - def test_v3_v2_intermix_trustor_not_in_default_domain_failed(self): - ref = self.new_trust_ref( - trustor_user_id=self.user_id, - trustee_user_id=self.default_domain_user_id, - project_id=self.project_id, - impersonation=False, - expires=dict(minutes=1), - role_ids=[self.role_id]) + # Bypass policy enforcement + with mock.patch.object(rules, 'enforce', return_value=True): + self.post('/OS-TRUST/trusts', + body={'trust': self.chained_trust_ref}, + token=trust_token, + expected_status=http_client.FORBIDDEN) - r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + def test_redelegation_terminator(self): + self.redelegated_trust_ref['expires_at'] = ( + datetime.datetime.utcnow().replace(year=2032).strftime( + unit.TIME_FORMAT)) + r = self.post('/OS-TRUST/trusts', + body={'trust': self.redelegated_trust_ref}) trust = self.assertValidTrustResponse(r) + trust_token = self._get_trust_token(trust) - auth_data = self.build_authentication_request( - user_id=self.default_domain_user['id'], - password=self.default_domain_user['password'], - trust_id=trust['id']) - r = self.v3_authenticate_token(auth_data) - self.assertValidProjectTrustScopedTokenResponse( - r, self.default_domain_user) + # Build second trust - the terminator + self.chained_trust_ref['expires_at'] = ( + datetime.datetime.utcnow().replace(year=2028).strftime( + unit.TIME_FORMAT)) + ref = dict(self.chained_trust_ref, + redelegation_count=1, + allow_redelegation=False) - token = r.headers.get('X-Subject-Token') + r = self.post('/OS-TRUST/trusts', + body={'trust': ref}, + token=trust_token) - # now validate the v3 token with v2 API - path = '/v2.0/tokens/%s' % (token) - self.admin_request( - path=path, token=CONF.admin_token, - method='GET', expected_status=http_client.UNAUTHORIZED) - - def test_v3_v2_intermix_trustor_not_in_default_domaini_failed(self): - ref = self.new_trust_ref( - trustor_user_id=self.default_domain_user_id, - trustee_user_id=self.trustee_user_id, - project_id=self.default_domain_project_id, - impersonation=False, - expires=dict(minutes=1), - role_ids=[self.role_id]) + trust = self.assertValidTrustResponse(r) + # Check that allow_redelegation == False caused redelegation_count + # to be set to 0, while allow_redelegation is removed + self.assertNotIn('allow_redelegation', trust) + self.assertEqual(0, trust['redelegation_count']) + trust_token = self._get_trust_token(trust) - auth_data = self.build_authentication_request( - user_id=self.default_domain_user['id'], - password=self.default_domain_user['password'], - project_id=self.default_domain_project_id) - token = self.get_requested_token(auth_data) + # Build third trust, same as second + self.post('/OS-TRUST/trusts', + body={'trust': ref}, + token=trust_token, + expected_status=http_client.FORBIDDEN) - r = self.post('/OS-TRUST/trusts', body={'trust': ref}, token=token) - trust = self.assertValidTrustResponse(r) + def test_redelegation_without_impersonation(self): + # Update trust to not allow impersonation + self.redelegated_trust_ref['impersonation'] = False + # Create trust + resp = self.post('/OS-TRUST/trusts', + body={'trust': self.redelegated_trust_ref}, + expected_status=http_client.CREATED) + trust = self.assertValidTrustResponse(resp) + + # Get trusted token without impersonation auth_data = self.build_authentication_request( user_id=self.trustee_user['id'], password=self.trustee_user['password'], trust_id=trust['id']) - r = self.v3_authenticate_token(auth_data) - self.assertValidProjectTrustScopedTokenResponse( - r, self.trustee_user) - token = r.headers.get('X-Subject-Token') + trust_token = self.get_requested_token(auth_data) - # now validate the v3 token with v2 API - path = '/v2.0/tokens/%s' % (token) - self.admin_request( - path=path, token=CONF.admin_token, - method='GET', expected_status=http_client.UNAUTHORIZED) - - def test_v3_v2_intermix_project_not_in_default_domaini_failed(self): - # create a trustee in default domain to delegate stuff to - trustee_user = self.new_user_ref(domain_id=test_v3.DEFAULT_DOMAIN_ID) - password = trustee_user['password'] - trustee_user = self.identity_api.create_user(trustee_user) - trustee_user['password'] = password - trustee_user_id = trustee_user['id'] - - ref = self.new_trust_ref( - trustor_user_id=self.default_domain_user_id, - trustee_user_id=trustee_user_id, + # Create second user for redelegation + trustee_user_2 = unit.create_user(self.identity_api, + domain_id=self.domain_id) + + # Trust for redelegation + trust_ref_2 = unit.new_trust_ref( + trustor_user_id=self.trustee_user['id'], + trustee_user_id=trustee_user_2['id'], project_id=self.project_id, impersonation=False, expires=dict(minutes=1), - role_ids=[self.role_id]) + role_ids=[self.role_id], + allow_redelegation=False) - auth_data = self.build_authentication_request( - user_id=self.default_domain_user['id'], - password=self.default_domain_user['password'], - project_id=self.default_domain_project_id) - token = self.get_requested_token(auth_data) + # Creating a second trust should not be allowed since trustor does not + # have the role to delegate thus returning 404 NOT FOUND. + resp = self.post('/OS-TRUST/trusts', + body={'trust': trust_ref_2}, + token=trust_token, + expected_status=http_client.NOT_FOUND) - r = self.post('/OS-TRUST/trusts', body={'trust': ref}, token=token) - trust = self.assertValidTrustResponse(r) + def test_create_unscoped_trust(self): + ref = unit.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user['id']) + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + self.assertValidTrustResponse(r, ref) - auth_data = self.build_authentication_request( - user_id=trustee_user['id'], - password=trustee_user['password'], - trust_id=trust['id']) - r = self.v3_authenticate_token(auth_data) - self.assertValidProjectTrustScopedTokenResponse( - r, trustee_user) - token = r.headers.get('X-Subject-Token') + def test_create_trust_no_roles(self): + ref = unit.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user['id'], + project_id=self.project_id) + self.post('/OS-TRUST/trusts', body={'trust': ref}, + expected_status=http_client.FORBIDDEN) - # now validate the v3 token with v2 API - path = '/v2.0/tokens/%s' % (token) - self.admin_request( - path=path, token=CONF.admin_token, - method='GET', expected_status=http_client.UNAUTHORIZED) - - def test_v3_v2_intermix(self): - # create a trustee in default domain to delegate stuff to - trustee_user = self.new_user_ref(domain_id=test_v3.DEFAULT_DOMAIN_ID) - password = trustee_user['password'] - trustee_user = self.identity_api.create_user(trustee_user) - trustee_user['password'] = password - trustee_user_id = trustee_user['id'] - - ref = self.new_trust_ref( - trustor_user_id=self.default_domain_user_id, - trustee_user_id=trustee_user_id, - project_id=self.default_domain_project_id, - impersonation=False, - expires=dict(minutes=1), + def _initialize_test_consume_trust(self, count): + # Make sure remaining_uses is decremented as we consume the trust + ref = unit.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user['id'], + project_id=self.project_id, + remaining_uses=count, role_ids=[self.role_id]) + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + # make sure the trust exists + trust = self.assertValidTrustResponse(r, ref) + r = self.get( + '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}) + # get a token for the trustee auth_data = self.build_authentication_request( - user_id=self.default_domain_user['id'], - password=self.default_domain_user['password'], - project_id=self.default_domain_project_id) - token = self.get_requested_token(auth_data) - - r = self.post('/OS-TRUST/trusts', body={'trust': ref}, token=token) - trust = self.assertValidTrustResponse(r) - + user_id=self.trustee_user['id'], + password=self.trustee_user['password']) + r = self.v3_create_token(auth_data) + token = r.headers.get('X-Subject-Token') + # get a trust token, consume one use auth_data = self.build_authentication_request( - user_id=trustee_user['id'], - password=trustee_user['password'], + token=token, trust_id=trust['id']) - r = self.v3_authenticate_token(auth_data) - self.assertValidProjectTrustScopedTokenResponse( - r, trustee_user) - token = r.headers.get('X-Subject-Token') - - # now validate the v3 token with v2 API - path = '/v2.0/tokens/%s' % (token) - self.admin_request( - path=path, token=CONF.admin_token, - method='GET', expected_status=200) - - def test_exercise_trust_scoped_token_without_impersonation(self): - ref = self.new_trust_ref( - trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user_id, - project_id=self.project_id, - impersonation=False, - expires=dict(minutes=1), - role_ids=[self.role_id]) + r = self.v3_create_token(auth_data) + return trust - r = self.post('/OS-TRUST/trusts', body={'trust': ref}) - trust = self.assertValidTrustResponse(r) + def test_consume_trust_once(self): + trust = self._initialize_test_consume_trust(2) + # check decremented value + r = self.get( + '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}) + trust = r.result.get('trust') + self.assertIsNotNone(trust) + self.assertEqual(1, trust['remaining_uses']) + # FIXME(lbragstad): Assert the role that is returned is the right role. + def test_create_one_time_use_trust(self): + trust = self._initialize_test_consume_trust(1) + # No more uses, the trust is made unavailable + self.get( + '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}, + expected_status=http_client.NOT_FOUND) + # this time we can't get a trust token auth_data = self.build_authentication_request( user_id=self.trustee_user['id'], password=self.trustee_user['password'], trust_id=trust['id']) - r = self.v3_authenticate_token(auth_data) - self.assertValidProjectTrustScopedTokenResponse(r, self.trustee_user) - self.assertEqual(self.trustee_user['id'], - r.result['token']['user']['id']) - self.assertEqual(self.trustee_user['name'], - r.result['token']['user']['name']) - self.assertEqual(self.domain['id'], - r.result['token']['user']['domain']['id']) - self.assertEqual(self.domain['name'], - r.result['token']['user']['domain']['name']) - self.assertEqual(self.project['id'], - r.result['token']['project']['id']) - self.assertEqual(self.project['name'], - r.result['token']['project']['name']) + self.v3_create_token(auth_data, + expected_status=http_client.UNAUTHORIZED) - def test_exercise_trust_scoped_token_with_impersonation(self): - ref = self.new_trust_ref( + def test_create_unlimited_use_trust(self): + # by default trusts are unlimited in terms of tokens that can be + # generated from them, this test creates such a trust explicitly + ref = unit.new_trust_ref( trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user_id, + trustee_user_id=self.trustee_user['id'], project_id=self.project_id, - impersonation=True, - expires=dict(minutes=1), + remaining_uses=None, role_ids=[self.role_id]) - r = self.post('/OS-TRUST/trusts', body={'trust': ref}) - trust = self.assertValidTrustResponse(r) + trust = self.assertValidTrustResponse(r, ref) + r = self.get( + '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}) auth_data = self.build_authentication_request( user_id=self.trustee_user['id'], - password=self.trustee_user['password'], + password=self.trustee_user['password']) + r = self.v3_create_token(auth_data) + token = r.headers.get('X-Subject-Token') + auth_data = self.build_authentication_request( + token=token, trust_id=trust['id']) - r = self.v3_authenticate_token(auth_data) - self.assertValidProjectTrustScopedTokenResponse(r, self.user) - self.assertEqual(self.user['id'], r.result['token']['user']['id']) - self.assertEqual(self.user['name'], r.result['token']['user']['name']) - self.assertEqual(self.domain['id'], - r.result['token']['user']['domain']['id']) - self.assertEqual(self.domain['name'], - r.result['token']['user']['domain']['name']) - self.assertEqual(self.project['id'], - r.result['token']['project']['id']) - self.assertEqual(self.project['name'], - r.result['token']['project']['name']) + r = self.v3_create_token(auth_data) + r = self.get( + '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}) + trust = r.result.get('trust') + self.assertIsNone(trust['remaining_uses']) def test_impersonation_token_cannot_create_new_trust(self): - ref = self.new_trust_ref( + ref = unit.new_trust_ref( trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user_id, + trustee_user_id=self.trustee_user['id'], project_id=self.project_id, impersonation=True, expires=dict(minutes=1), @@ -3653,9 +4002,9 @@ class TestTrustAuth(test_v3.RestfulTestCase): trust_token = self.get_requested_token(auth_data) # Build second trust - ref = self.new_trust_ref( + ref = unit.new_trust_ref( trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user_id, + trustee_user_id=self.trustee_user['id'], project_id=self.project_id, impersonation=True, expires=dict(minutes=1), @@ -3668,7 +4017,7 @@ class TestTrustAuth(test_v3.RestfulTestCase): def test_trust_deleted_grant(self): # create a new role - role = self.new_role_ref() + role = unit.new_role_ref() self.role_api.create_role(role['id'], role) grant_url = ( @@ -3682,9 +4031,9 @@ class TestTrustAuth(test_v3.RestfulTestCase): self.put(grant_url) # create a trust that delegates the new role - ref = self.new_trust_ref( + ref = unit.new_trust_ref( trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user_id, + trustee_user_id=self.trustee_user['id'], project_id=self.project_id, impersonation=False, expires=dict(minutes=1), @@ -3702,8 +4051,8 @@ class TestTrustAuth(test_v3.RestfulTestCase): user_id=self.trustee_user['id'], password=self.trustee_user['password'], trust_id=trust['id']) - r = self.v3_authenticate_token(auth_data, - expected_status=http_client.FORBIDDEN) + r = self.v3_create_token(auth_data, + expected_status=http_client.FORBIDDEN) def test_trust_chained(self): """Test that a trust token can't be used to execute another trust. @@ -3713,28 +4062,26 @@ class TestTrustAuth(test_v3.RestfulTestCase): """ # create a sub-trustee user - sub_trustee_user = self.new_user_ref( + sub_trustee_user = unit.create_user( + self.identity_api, domain_id=test_v3.DEFAULT_DOMAIN_ID) - password = sub_trustee_user['password'] - sub_trustee_user = self.identity_api.create_user(sub_trustee_user) - sub_trustee_user['password'] = password sub_trustee_user_id = sub_trustee_user['id'] # create a new role - role = self.new_role_ref() + role = unit.new_role_ref() self.role_api.create_role(role['id'], role) # assign the new role to trustee self.put( '/projects/%(project_id)s/users/%(user_id)s/roles/%(role_id)s' % { 'project_id': self.project_id, - 'user_id': self.trustee_user_id, + 'user_id': self.trustee_user['id'], 'role_id': role['id']}) # create a trust from trustor -> trustee - ref = self.new_trust_ref( + ref = unit.new_trust_ref( trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user_id, + trustee_user_id=self.trustee_user['id'], project_id=self.project_id, impersonation=True, expires=dict(minutes=1), @@ -3744,14 +4091,14 @@ class TestTrustAuth(test_v3.RestfulTestCase): # authenticate as trustee so we can create a second trust auth_data = self.build_authentication_request( - user_id=self.trustee_user_id, + user_id=self.trustee_user['id'], password=self.trustee_user['password'], project_id=self.project_id) token = self.get_requested_token(auth_data) # create a trust from trustee -> sub-trustee - ref = self.new_trust_ref( - trustor_user_id=self.trustee_user_id, + ref = unit.new_trust_ref( + trustor_user_id=self.trustee_user['id'], trustee_user_id=sub_trustee_user_id, project_id=self.project_id, impersonation=True, @@ -3771,12 +4118,11 @@ class TestTrustAuth(test_v3.RestfulTestCase): auth_data = self.build_authentication_request( token=trust_token, trust_id=trust1['id']) - r = self.v3_authenticate_token(auth_data, - expected_status=http_client.FORBIDDEN) + r = self.v3_create_token(auth_data, + expected_status=http_client.FORBIDDEN) def assertTrustTokensRevoked(self, trust_id): - revocation_response = self.get('/OS-REVOKE/events', - expected_status=200) + revocation_response = self.get('/OS-REVOKE/events') revocation_events = revocation_response.json_body['events'] found = False for event in revocation_events: @@ -3786,9 +4132,9 @@ class TestTrustAuth(test_v3.RestfulTestCase): trust_id) def test_delete_trust_revokes_tokens(self): - ref = self.new_trust_ref( + ref = unit.new_trust_ref( trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user_id, + trustee_user_id=self.trustee_user['id'], project_id=self.project_id, impersonation=False, expires=dict(minutes=1), @@ -3800,13 +4146,12 @@ class TestTrustAuth(test_v3.RestfulTestCase): user_id=self.trustee_user['id'], password=self.trustee_user['password'], trust_id=trust_id) - r = self.v3_authenticate_token(auth_data) - self.assertValidProjectTrustScopedTokenResponse( + r = self.v3_create_token(auth_data) + self.assertValidProjectScopedTokenResponse( r, self.trustee_user) trust_token = r.headers['X-Subject-Token'] self.delete('/OS-TRUST/trusts/%(trust_id)s' % { - 'trust_id': trust_id}, - expected_status=204) + 'trust_id': trust_id}) headers = {'X-Subject-Token': trust_token} self.head('/auth/tokens', headers=headers, expected_status=http_client.NOT_FOUND) @@ -3817,9 +4162,9 @@ class TestTrustAuth(test_v3.RestfulTestCase): self.identity_api.update_user(user['id'], user) def test_trust_get_token_fails_if_trustor_disabled(self): - ref = self.new_trust_ref( + ref = unit.new_trust_ref( trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user_id, + trustee_user_id=self.trustee_user['id'], project_id=self.project_id, impersonation=False, expires=dict(minutes=1), @@ -3833,7 +4178,7 @@ class TestTrustAuth(test_v3.RestfulTestCase): user_id=self.trustee_user['id'], password=self.trustee_user['password'], trust_id=trust['id']) - self.v3_authenticate_token(auth_data, expected_status=201) + self.v3_create_token(auth_data) self.disable_user(self.user) @@ -3841,13 +4186,13 @@ class TestTrustAuth(test_v3.RestfulTestCase): user_id=self.trustee_user['id'], password=self.trustee_user['password'], trust_id=trust['id']) - self.v3_authenticate_token(auth_data, - expected_status=http_client.FORBIDDEN) + self.v3_create_token(auth_data, + expected_status=http_client.FORBIDDEN) def test_trust_get_token_fails_if_trustee_disabled(self): - ref = self.new_trust_ref( + ref = unit.new_trust_ref( trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user_id, + trustee_user_id=self.trustee_user['id'], project_id=self.project_id, impersonation=False, expires=dict(minutes=1), @@ -3861,7 +4206,7 @@ class TestTrustAuth(test_v3.RestfulTestCase): user_id=self.trustee_user['id'], password=self.trustee_user['password'], trust_id=trust['id']) - self.v3_authenticate_token(auth_data, expected_status=201) + self.v3_create_token(auth_data) self.disable_user(self.trustee_user) @@ -3869,13 +4214,13 @@ class TestTrustAuth(test_v3.RestfulTestCase): user_id=self.trustee_user['id'], password=self.trustee_user['password'], trust_id=trust['id']) - self.v3_authenticate_token(auth_data, - expected_status=http_client.UNAUTHORIZED) + self.v3_create_token(auth_data, + expected_status=http_client.UNAUTHORIZED) def test_delete_trust(self): - ref = self.new_trust_ref( + ref = unit.new_trust_ref( trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user_id, + trustee_user_id=self.trustee_user['id'], project_id=self.project_id, impersonation=False, expires=dict(minutes=1), @@ -3886,57 +4231,19 @@ class TestTrustAuth(test_v3.RestfulTestCase): trust = self.assertValidTrustResponse(r, ref) self.delete('/OS-TRUST/trusts/%(trust_id)s' % { - 'trust_id': trust['id']}, - expected_status=204) - - self.get('/OS-TRUST/trusts/%(trust_id)s' % { - 'trust_id': trust['id']}, - expected_status=http_client.NOT_FOUND) - - self.get('/OS-TRUST/trusts/%(trust_id)s' % { - 'trust_id': trust['id']}, - expected_status=http_client.NOT_FOUND) + 'trust_id': trust['id']}) auth_data = self.build_authentication_request( user_id=self.trustee_user['id'], password=self.trustee_user['password'], trust_id=trust['id']) - self.v3_authenticate_token(auth_data, - expected_status=http_client.UNAUTHORIZED) - - def test_list_trusts(self): - ref = self.new_trust_ref( - trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user_id, - project_id=self.project_id, - impersonation=False, - expires=dict(minutes=1), - role_ids=[self.role_id]) - - for i in range(3): - r = self.post('/OS-TRUST/trusts', body={'trust': ref}) - self.assertValidTrustResponse(r, ref) - - r = self.get('/OS-TRUST/trusts', expected_status=200) - trusts = r.result['trusts'] - self.assertEqual(3, len(trusts)) - self.assertValidTrustListResponse(r) - - r = self.get('/OS-TRUST/trusts?trustor_user_id=%s' % - self.user_id, expected_status=200) - trusts = r.result['trusts'] - self.assertEqual(3, len(trusts)) - self.assertValidTrustListResponse(r) - - r = self.get('/OS-TRUST/trusts?trustee_user_id=%s' % - self.user_id, expected_status=200) - trusts = r.result['trusts'] - self.assertEqual(0, len(trusts)) + self.v3_create_token(auth_data, + expected_status=http_client.UNAUTHORIZED) def test_change_password_invalidates_trust_tokens(self): - ref = self.new_trust_ref( + ref = unit.new_trust_ref( trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user_id, + trustee_user_id=self.trustee_user['id'], project_id=self.project_id, impersonation=True, expires=dict(minutes=1), @@ -3949,64 +4256,52 @@ class TestTrustAuth(test_v3.RestfulTestCase): user_id=self.trustee_user['id'], password=self.trustee_user['password'], trust_id=trust['id']) - r = self.v3_authenticate_token(auth_data) + r = self.v3_create_token(auth_data) - self.assertValidProjectTrustScopedTokenResponse(r, self.user) + self.assertValidProjectScopedTokenResponse(r, self.user) trust_token = r.headers.get('X-Subject-Token') self.get('/OS-TRUST/trusts?trustor_user_id=%s' % - self.user_id, expected_status=200, - token=trust_token) + self.user_id, token=trust_token) self.assertValidUserResponse( self.patch('/users/%s' % self.trustee_user['id'], - body={'user': {'password': uuid.uuid4().hex}}, - expected_status=200)) + body={'user': {'password': uuid.uuid4().hex}})) self.get('/OS-TRUST/trusts?trustor_user_id=%s' % self.user_id, expected_status=http_client.UNAUTHORIZED, token=trust_token) def test_trustee_can_do_role_ops(self): - ref = self.new_trust_ref( - trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user_id, - project_id=self.project_id, - impersonation=True, - role_ids=[self.role_id]) - - r = self.post('/OS-TRUST/trusts', body={'trust': ref}) - trust = self.assertValidTrustResponse(r) - - auth_data = self.build_authentication_request( - user_id=self.trustee_user['id'], - password=self.trustee_user['password']) + resp = self.post('/OS-TRUST/trusts', + body={'trust': self.redelegated_trust_ref}) + trust = self.assertValidTrustResponse(resp) + trust_token = self._get_trust_token(trust) - r = self.get( + resp = self.get( '/OS-TRUST/trusts/%(trust_id)s/roles' % { 'trust_id': trust['id']}, - auth=auth_data) - self.assertValidRoleListResponse(r, self.role) + token=trust_token) + self.assertValidRoleListResponse(resp, self.role) self.head( '/OS-TRUST/trusts/%(trust_id)s/roles/%(role_id)s' % { 'trust_id': trust['id'], 'role_id': self.role['id']}, - auth=auth_data, - expected_status=200) + token=trust_token, + expected_status=http_client.OK) - r = self.get( + resp = self.get( '/OS-TRUST/trusts/%(trust_id)s/roles/%(role_id)s' % { 'trust_id': trust['id'], 'role_id': self.role['id']}, - auth=auth_data, - expected_status=200) - self.assertValidRoleResponse(r, self.role) + token=trust_token) + self.assertValidRoleResponse(resp, self.role) def test_do_not_consume_remaining_uses_when_get_token_fails(self): - ref = self.new_trust_ref( + ref = unit.new_trust_ref( trustor_user_id=self.user_id, - trustee_user_id=self.trustee_user_id, + trustee_user_id=self.trustee_user['id'], project_id=self.project_id, impersonation=False, expires=dict(minutes=1), @@ -4023,536 +4318,360 @@ class TestTrustAuth(test_v3.RestfulTestCase): user_id=self.default_domain_user['id'], password=self.default_domain_user['password'], trust_id=trust_id) - self.v3_authenticate_token(auth_data, - expected_status=http_client.FORBIDDEN) + self.v3_create_token(auth_data, + expected_status=http_client.FORBIDDEN) r = self.get('/OS-TRUST/trusts/%s' % trust_id) self.assertEqual(3, r.result.get('trust').get('remaining_uses')) -class TestAPIProtectionWithoutAuthContextMiddleware(test_v3.RestfulTestCase): - def test_api_protection_with_no_auth_context_in_env(self): - auth_data = self.build_authentication_request( - user_id=self.default_domain_user['id'], - password=self.default_domain_user['password'], - project_id=self.project['id']) - token = self.get_requested_token(auth_data) - auth_controller = auth.controllers.Auth() - # all we care is that auth context is not in the environment and - # 'token_id' is used to build the auth context instead - context = {'subject_token_id': token, - 'token_id': token, - 'query_string': {}, - 'environment': {}} - r = auth_controller.validate_token(context) - self.assertEqual(200, r.status_code) +class TestTrustChain(test_v3.RestfulTestCase): + def config_overrides(self): + super(TestTrustChain, self).config_overrides() + self.config_fixture.config( + group='trust', + enabled=True, + allow_redelegation=True, + max_redelegation_count=10 + ) -class TestAuthContext(unit.TestCase): def setUp(self): - super(TestAuthContext, self).setUp() - self.auth_context = auth.controllers.AuthContext() - - def test_pick_lowest_expires_at(self): - expires_at_1 = utils.isotime(timeutils.utcnow()) - expires_at_2 = utils.isotime(timeutils.utcnow() + - datetime.timedelta(seconds=10)) - # make sure auth_context picks the lowest value - self.auth_context['expires_at'] = expires_at_1 - self.auth_context['expires_at'] = expires_at_2 - self.assertEqual(expires_at_1, self.auth_context['expires_at']) - - def test_identity_attribute_conflict(self): - for identity_attr in auth.controllers.AuthContext.IDENTITY_ATTRIBUTES: - self.auth_context[identity_attr] = uuid.uuid4().hex - if identity_attr == 'expires_at': - # 'expires_at' is a special case. Will test it in a separate - # test case. - continue - self.assertRaises(exception.Unauthorized, - operator.setitem, - self.auth_context, - identity_attr, - uuid.uuid4().hex) - - def test_identity_attribute_conflict_with_none_value(self): - for identity_attr in auth.controllers.AuthContext.IDENTITY_ATTRIBUTES: - self.auth_context[identity_attr] = None - - if identity_attr == 'expires_at': - # 'expires_at' is a special case and is tested above. - self.auth_context['expires_at'] = uuid.uuid4().hex - continue - - self.assertRaises(exception.Unauthorized, - operator.setitem, - self.auth_context, - identity_attr, - uuid.uuid4().hex) - - def test_non_identity_attribute_conflict_override(self): - # for attributes Keystone doesn't know about, make sure they can be - # freely manipulated - attr_name = uuid.uuid4().hex - attr_val_1 = uuid.uuid4().hex - attr_val_2 = uuid.uuid4().hex - self.auth_context[attr_name] = attr_val_1 - self.auth_context[attr_name] = attr_val_2 - self.assertEqual(attr_val_2, self.auth_context[attr_name]) - - -class TestAuthSpecificData(test_v3.RestfulTestCase): - - def test_get_catalog_project_scoped_token(self): - """Call ``GET /auth/catalog`` with a project-scoped token.""" - r = self.get( - '/auth/catalog', - expected_status=200) - self.assertValidCatalogResponse(r) - - def test_get_catalog_domain_scoped_token(self): - """Call ``GET /auth/catalog`` with a domain-scoped token.""" - # grant a domain role to a user - self.put(path='/domains/%s/users/%s/roles/%s' % ( - self.domain['id'], self.user['id'], self.role['id'])) - - self.get( - '/auth/catalog', - auth=self.build_authentication_request( - user_id=self.user['id'], - password=self.user['password'], - domain_id=self.domain['id']), - expected_status=http_client.FORBIDDEN) - - def test_get_catalog_unscoped_token(self): - """Call ``GET /auth/catalog`` with an unscoped token.""" - self.get( - '/auth/catalog', - auth=self.build_authentication_request( - user_id=self.default_domain_user['id'], - password=self.default_domain_user['password']), - expected_status=http_client.FORBIDDEN) - - def test_get_catalog_no_token(self): - """Call ``GET /auth/catalog`` without a token.""" - self.get( - '/auth/catalog', - noauth=True, - expected_status=http_client.UNAUTHORIZED) - - def test_get_projects_project_scoped_token(self): - r = self.get('/auth/projects', expected_status=200) - self.assertThat(r.json['projects'], matchers.HasLength(1)) - self.assertValidProjectListResponse(r) - - def test_get_domains_project_scoped_token(self): - self.put(path='/domains/%s/users/%s/roles/%s' % ( - self.domain['id'], self.user['id'], self.role['id'])) - - r = self.get('/auth/domains', expected_status=200) - self.assertThat(r.json['domains'], matchers.HasLength(1)) - self.assertValidDomainListResponse(r) + super(TestTrustChain, self).setUp() + """Create a trust chain using redelegation. + + A trust chain is a series of trusts that are redelegated. For example, + self.user_list consists of userA, userB, and userC. The first trust in + the trust chain is going to be established between self.user and userA, + call it trustA. Then, userA is going to obtain a trust scoped token + using trustA, and with that token create a trust between userA and + userB called trustB. This pattern will continue with userB creating a + trust with userC. + So the trust chain should look something like: + trustA -> trustB -> trustC + Where: + self.user is trusting userA with trustA + userA is trusting userB with trustB + userB is trusting userC with trustC + """ + self.user_list = list() + self.trust_chain = list() + for _ in range(3): + user = unit.create_user(self.identity_api, + domain_id=self.domain_id) + self.user_list.append(user) -class TestFernetTokenProvider(test_v3.RestfulTestCase): - def setUp(self): - super(TestFernetTokenProvider, self).setUp() - self.useFixture(ksfixtures.KeyRepository(self.config_fixture)) + # trustor->trustee redelegation with impersonation + trustee = self.user_list[0] + trust_ref = unit.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=trustee['id'], + project_id=self.project_id, + impersonation=True, + expires=dict(minutes=1), + role_ids=[self.role_id], + allow_redelegation=True, + redelegation_count=3) - def _make_auth_request(self, auth_data): - resp = self.post('/auth/tokens', body=auth_data, expected_status=201) - token = resp.headers.get('X-Subject-Token') - self.assertLess(len(token), 255) - return token + # Create a trust between self.user and the first user in the list + r = self.post('/OS-TRUST/trusts', + body={'trust': trust_ref}) - def _get_unscoped_token(self): + trust = self.assertValidTrustResponse(r) auth_data = self.build_authentication_request( - user_id=self.user['id'], - password=self.user['password']) - return self._make_auth_request(auth_data) + user_id=trustee['id'], + password=trustee['password'], + trust_id=trust['id']) - def _get_project_scoped_token(self): - auth_data = self.build_authentication_request( - user_id=self.user['id'], - password=self.user['password'], - project_id=self.project_id) - return self._make_auth_request(auth_data) + # Generate a trusted token for the first user + trust_token = self.get_requested_token(auth_data) + self.trust_chain.append(trust) - def _get_domain_scoped_token(self): - auth_data = self.build_authentication_request( - user_id=self.user['id'], - password=self.user['password'], - domain_id=self.domain_id) - return self._make_auth_request(auth_data) + # Loop through the user to create a chain of redelegated trust. + for next_trustee in self.user_list[1:]: + trust_ref = unit.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=next_trustee['id'], + project_id=self.project_id, + impersonation=True, + role_ids=[self.role_id], + allow_redelegation=True) + r = self.post('/OS-TRUST/trusts', + body={'trust': trust_ref}, + token=trust_token) + trust = self.assertValidTrustResponse(r) + auth_data = self.build_authentication_request( + user_id=next_trustee['id'], + password=next_trustee['password'], + trust_id=trust['id']) + trust_token = self.get_requested_token(auth_data) + self.trust_chain.append(trust) - def _get_trust_scoped_token(self, trustee_user, trust): + trustee = self.user_list[-1] + trust = self.trust_chain[-1] auth_data = self.build_authentication_request( - user_id=trustee_user['id'], - password=trustee_user['password'], + user_id=trustee['id'], + password=trustee['password'], trust_id=trust['id']) - return self._make_auth_request(auth_data) - def _validate_token(self, token, expected_status=200): - return self.get( - '/auth/tokens', - headers={'X-Subject-Token': token}, - expected_status=expected_status) + self.last_token = self.get_requested_token(auth_data) - def _revoke_token(self, token, expected_status=204): - return self.delete( - '/auth/tokens', - headers={'X-Subject-Token': token}, - expected_status=expected_status) + def assert_user_authenticate(self, user): + auth_data = self.build_authentication_request( + user_id=user['id'], + password=user['password'] + ) + r = self.v3_create_token(auth_data) + self.assertValidTokenResponse(r) - def _set_user_enabled(self, user, enabled=True): - user['enabled'] = enabled - self.identity_api.update_user(user['id'], user) + def assert_trust_tokens_revoked(self, trust_id): + trustee = self.user_list[0] + auth_data = self.build_authentication_request( + user_id=trustee['id'], + password=trustee['password'] + ) + r = self.v3_create_token(auth_data) + self.assertValidTokenResponse(r) - def _create_trust(self): - # Create a trustee user - trustee_user_ref = self.new_user_ref(domain_id=self.domain_id) - trustee_user = self.identity_api.create_user(trustee_user_ref) - trustee_user['password'] = trustee_user_ref['password'] - ref = self.new_trust_ref( - trustor_user_id=self.user_id, - trustee_user_id=trustee_user['id'], - project_id=self.project_id, - impersonation=False, - role_ids=[self.role_id]) + revocation_response = self.get('/OS-REVOKE/events') + revocation_events = revocation_response.json_body['events'] + found = False + for event in revocation_events: + if event.get('OS-TRUST:trust_id') == trust_id: + found = True + self.assertTrue(found, 'event with trust_id %s not found in list' % + trust_id) - # Create a trust - r = self.post('/OS-TRUST/trusts', body={'trust': ref}) - trust = self.assertValidTrustResponse(r) - return (trustee_user, trust) + def test_delete_trust_cascade(self): + self.assert_user_authenticate(self.user_list[0]) + self.delete('/OS-TRUST/trusts/%(trust_id)s' % { + 'trust_id': self.trust_chain[0]['id']}) - def config_overrides(self): - super(TestFernetTokenProvider, self).config_overrides() - self.config_fixture.config(group='token', provider='fernet') + headers = {'X-Subject-Token': self.last_token} + self.head('/auth/tokens', headers=headers, + expected_status=http_client.NOT_FOUND) + self.assert_trust_tokens_revoked(self.trust_chain[0]['id']) - def test_validate_unscoped_token(self): - unscoped_token = self._get_unscoped_token() - self._validate_token(unscoped_token) + def test_delete_broken_chain(self): + self.assert_user_authenticate(self.user_list[0]) + self.delete('/OS-TRUST/trusts/%(trust_id)s' % { + 'trust_id': self.trust_chain[0]['id']}) - def test_validate_tampered_unscoped_token_fails(self): - unscoped_token = self._get_unscoped_token() - tampered_token = (unscoped_token[:50] + uuid.uuid4().hex + - unscoped_token[50 + 32:]) - self._validate_token(tampered_token, - expected_status=http_client.NOT_FOUND) + # Verify the two remaining trust have been deleted + for i in range(len(self.user_list) - 1): + auth_data = self.build_authentication_request( + user_id=self.user_list[i]['id'], + password=self.user_list[i]['password']) - def test_revoke_unscoped_token(self): - unscoped_token = self._get_unscoped_token() - self._validate_token(unscoped_token) - self._revoke_token(unscoped_token) - self._validate_token(unscoped_token, - expected_status=http_client.NOT_FOUND) + auth_token = self.get_requested_token(auth_data) - def test_unscoped_token_is_invalid_after_disabling_user(self): - unscoped_token = self._get_unscoped_token() - # Make sure the token is valid - self._validate_token(unscoped_token) - # Disable the user - self._set_user_enabled(self.user, enabled=False) - # Ensure validating a token for a disabled user fails - self.assertRaises(exception.TokenNotFound, - self.token_provider_api.validate_token, - unscoped_token) + # Assert chained trust have been deleted + self.get('/OS-TRUST/trusts/%(trust_id)s' % { + 'trust_id': self.trust_chain[i + 1]['id']}, + token=auth_token, + expected_status=http_client.NOT_FOUND) - def test_unscoped_token_is_invalid_after_enabling_disabled_user(self): - unscoped_token = self._get_unscoped_token() - # Make sure the token is valid - self._validate_token(unscoped_token) - # Disable the user - self._set_user_enabled(self.user, enabled=False) - # Ensure validating a token for a disabled user fails - self.assertRaises(exception.TokenNotFound, - self.token_provider_api.validate_token, - unscoped_token) - # Enable the user - self._set_user_enabled(self.user) - # Ensure validating a token for a re-enabled user fails - self.assertRaises(exception.TokenNotFound, - self.token_provider_api.validate_token, - unscoped_token) + def test_trustor_roles_revoked(self): + self.assert_user_authenticate(self.user_list[0]) - def test_unscoped_token_is_invalid_after_disabling_user_domain(self): - unscoped_token = self._get_unscoped_token() - # Make sure the token is valid - self._validate_token(unscoped_token) - # Disable the user's domain - self.domain['enabled'] = False - self.resource_api.update_domain(self.domain['id'], self.domain) - # Ensure validating a token for a disabled user fails - self.assertRaises(exception.TokenNotFound, - self.token_provider_api.validate_token, - unscoped_token) + self.assignment_api.remove_role_from_user_and_project( + self.user_id, self.project_id, self.role_id + ) - def test_unscoped_token_is_invalid_after_changing_user_password(self): - unscoped_token = self._get_unscoped_token() - # Make sure the token is valid - self._validate_token(unscoped_token) - # Change user's password - self.user['password'] = 'Password1' - self.identity_api.update_user(self.user['id'], self.user) - # Ensure updating user's password revokes existing user's tokens - self.assertRaises(exception.TokenNotFound, - self.token_provider_api.validate_token, - unscoped_token) + # Verify that users are not allowed to authenticate with trust + for i in range(len(self.user_list[1:])): + trustee = self.user_list[i] + auth_data = self.build_authentication_request( + user_id=trustee['id'], + password=trustee['password']) - def test_validate_project_scoped_token(self): - project_scoped_token = self._get_project_scoped_token() - self._validate_token(project_scoped_token) + # Attempt to authenticate with trust + token = self.get_requested_token(auth_data) + auth_data = self.build_authentication_request( + token=token, + trust_id=self.trust_chain[i - 1]['id']) - def test_validate_domain_scoped_token(self): - # Grant user access to domain - self.assignment_api.create_grant(self.role['id'], - user_id=self.user['id'], - domain_id=self.domain['id']) - domain_scoped_token = self._get_domain_scoped_token() - resp = self._validate_token(domain_scoped_token) - resp_json = json.loads(resp.body) - self.assertIsNotNone(resp_json['token']['catalog']) - self.assertIsNotNone(resp_json['token']['roles']) - self.assertIsNotNone(resp_json['token']['domain']) + # Trustee has no delegated roles + self.v3_create_token(auth_data, + expected_status=http_client.FORBIDDEN) - def test_validate_tampered_project_scoped_token_fails(self): - project_scoped_token = self._get_project_scoped_token() - tampered_token = (project_scoped_token[:50] + uuid.uuid4().hex + - project_scoped_token[50 + 32:]) - self._validate_token(tampered_token, - expected_status=http_client.NOT_FOUND) + def test_intermediate_user_disabled(self): + self.assert_user_authenticate(self.user_list[0]) - def test_revoke_project_scoped_token(self): - project_scoped_token = self._get_project_scoped_token() - self._validate_token(project_scoped_token) - self._revoke_token(project_scoped_token) - self._validate_token(project_scoped_token, - expected_status=http_client.NOT_FOUND) + disabled = self.user_list[0] + disabled['enabled'] = False + self.identity_api.update_user(disabled['id'], disabled) - def test_project_scoped_token_is_invalid_after_disabling_user(self): - project_scoped_token = self._get_project_scoped_token() - # Make sure the token is valid - self._validate_token(project_scoped_token) - # Disable the user - self._set_user_enabled(self.user, enabled=False) - # Ensure validating a token for a disabled user fails - self.assertRaises(exception.TokenNotFound, - self.token_provider_api.validate_token, - project_scoped_token) + # Bypass policy enforcement + with mock.patch.object(rules, 'enforce', return_value=True): + headers = {'X-Subject-Token': self.last_token} + self.head('/auth/tokens', headers=headers, + expected_status=http_client.FORBIDDEN) - def test_domain_scoped_token_is_invalid_after_disabling_user(self): - # Grant user access to domain - self.assignment_api.create_grant(self.role['id'], - user_id=self.user['id'], - domain_id=self.domain['id']) - domain_scoped_token = self._get_domain_scoped_token() - # Make sure the token is valid - self._validate_token(domain_scoped_token) - # Disable user - self._set_user_enabled(self.user, enabled=False) - # Ensure validating a token for a disabled user fails - self.assertRaises(exception.TokenNotFound, - self.token_provider_api.validate_token, - domain_scoped_token) + def test_intermediate_user_deleted(self): + self.assert_user_authenticate(self.user_list[0]) - def test_domain_scoped_token_is_invalid_after_deleting_grant(self): - # Grant user access to domain - self.assignment_api.create_grant(self.role['id'], - user_id=self.user['id'], - domain_id=self.domain['id']) - domain_scoped_token = self._get_domain_scoped_token() - # Make sure the token is valid - self._validate_token(domain_scoped_token) - # Delete access to domain - self.assignment_api.delete_grant(self.role['id'], - user_id=self.user['id'], - domain_id=self.domain['id']) - # Ensure validating a token for a disabled user fails - self.assertRaises(exception.TokenNotFound, - self.token_provider_api.validate_token, - domain_scoped_token) + self.identity_api.delete_user(self.user_list[0]['id']) - def test_project_scoped_token_invalid_after_changing_user_password(self): - project_scoped_token = self._get_project_scoped_token() - # Make sure the token is valid - self._validate_token(project_scoped_token) - # Update user's password - self.user['password'] = 'Password1' - self.identity_api.update_user(self.user['id'], self.user) - # Ensure updating user's password revokes existing tokens - self.assertRaises(exception.TokenNotFound, - self.token_provider_api.validate_token, - project_scoped_token) + # Bypass policy enforcement + with mock.patch.object(rules, 'enforce', return_value=True): + headers = {'X-Subject-Token': self.last_token} + self.head('/auth/tokens', headers=headers, + expected_status=http_client.FORBIDDEN) - def test_project_scoped_token_invalid_after_disabling_project(self): - project_scoped_token = self._get_project_scoped_token() - # Make sure the token is valid - self._validate_token(project_scoped_token) - # Disable project - self.project['enabled'] = False - self.resource_api.update_project(self.project['id'], self.project) - # Ensure validating a token for a disabled project fails - self.assertRaises(exception.TokenNotFound, - self.token_provider_api.validate_token, - project_scoped_token) - def test_domain_scoped_token_invalid_after_disabling_domain(self): - # Grant user access to domain - self.assignment_api.create_grant(self.role['id'], - user_id=self.user['id'], - domain_id=self.domain['id']) - domain_scoped_token = self._get_domain_scoped_token() - # Make sure the token is valid - self._validate_token(domain_scoped_token) - # Disable domain - self.domain['enabled'] = False - self.resource_api.update_domain(self.domain['id'], self.domain) - # Ensure validating a token for a disabled domain fails - self.assertRaises(exception.TokenNotFound, - self.token_provider_api.validate_token, - domain_scoped_token) +class TestAPIProtectionWithoutAuthContextMiddleware(test_v3.RestfulTestCase): + def test_api_protection_with_no_auth_context_in_env(self): + auth_data = self.build_authentication_request( + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password'], + project_id=self.project['id']) + token = self.get_requested_token(auth_data) + auth_controller = auth.controllers.Auth() + # all we care is that auth context is not in the environment and + # 'token_id' is used to build the auth context instead + context = {'subject_token_id': token, + 'token_id': token, + 'query_string': {}, + 'environment': {}} + r = auth_controller.validate_token(context) + self.assertEqual(http_client.OK, r.status_code) - def test_rescope_unscoped_token_with_trust(self): - trustee_user, trust = self._create_trust() - trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) - self.assertLess(len(trust_scoped_token), 255) - def test_validate_a_trust_scoped_token(self): - trustee_user, trust = self._create_trust() - trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) - # Validate a trust scoped token - self._validate_token(trust_scoped_token) +class TestAuthContext(unit.TestCase): + def setUp(self): + super(TestAuthContext, self).setUp() + self.auth_context = auth.controllers.AuthContext() - def test_validate_tampered_trust_scoped_token_fails(self): - trustee_user, trust = self._create_trust() - trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) - # Get a trust scoped token - tampered_token = (trust_scoped_token[:50] + uuid.uuid4().hex + - trust_scoped_token[50 + 32:]) - self._validate_token(tampered_token, - expected_status=http_client.NOT_FOUND) + def test_pick_lowest_expires_at(self): + expires_at_1 = utils.isotime(timeutils.utcnow()) + expires_at_2 = utils.isotime(timeutils.utcnow() + + datetime.timedelta(seconds=10)) + # make sure auth_context picks the lowest value + self.auth_context['expires_at'] = expires_at_1 + self.auth_context['expires_at'] = expires_at_2 + self.assertEqual(expires_at_1, self.auth_context['expires_at']) - def test_revoke_trust_scoped_token(self): - trustee_user, trust = self._create_trust() - trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) - # Validate a trust scoped token - self._validate_token(trust_scoped_token) - self._revoke_token(trust_scoped_token) - self._validate_token(trust_scoped_token, - expected_status=http_client.NOT_FOUND) + def test_identity_attribute_conflict(self): + for identity_attr in auth.controllers.AuthContext.IDENTITY_ATTRIBUTES: + self.auth_context[identity_attr] = uuid.uuid4().hex + if identity_attr == 'expires_at': + # 'expires_at' is a special case. Will test it in a separate + # test case. + continue + self.assertRaises(exception.Unauthorized, + operator.setitem, + self.auth_context, + identity_attr, + uuid.uuid4().hex) - def test_trust_scoped_token_is_invalid_after_disabling_trustee(self): - trustee_user, trust = self._create_trust() - trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) - # Validate a trust scoped token - self._validate_token(trust_scoped_token) + def test_identity_attribute_conflict_with_none_value(self): + for identity_attr in auth.controllers.AuthContext.IDENTITY_ATTRIBUTES: + self.auth_context[identity_attr] = None - # Disable trustee - trustee_update_ref = dict(enabled=False) - self.identity_api.update_user(trustee_user['id'], trustee_update_ref) - # Ensure validating a token for a disabled user fails - self.assertRaises(exception.TokenNotFound, - self.token_provider_api.validate_token, - trust_scoped_token) + if identity_attr == 'expires_at': + # 'expires_at' is a special case and is tested above. + self.auth_context['expires_at'] = uuid.uuid4().hex + continue - def test_trust_scoped_token_invalid_after_changing_trustee_password(self): - trustee_user, trust = self._create_trust() - trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) - # Validate a trust scoped token - self._validate_token(trust_scoped_token) - # Change trustee's password - trustee_update_ref = dict(password='Password1') - self.identity_api.update_user(trustee_user['id'], trustee_update_ref) - # Ensure updating trustee's password revokes existing tokens - self.assertRaises(exception.TokenNotFound, - self.token_provider_api.validate_token, - trust_scoped_token) + self.assertRaises(exception.Unauthorized, + operator.setitem, + self.auth_context, + identity_attr, + uuid.uuid4().hex) - def test_trust_scoped_token_is_invalid_after_disabling_trustor(self): - trustee_user, trust = self._create_trust() - trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) - # Validate a trust scoped token - self._validate_token(trust_scoped_token) + def test_non_identity_attribute_conflict_override(self): + # for attributes Keystone doesn't know about, make sure they can be + # freely manipulated + attr_name = uuid.uuid4().hex + attr_val_1 = uuid.uuid4().hex + attr_val_2 = uuid.uuid4().hex + self.auth_context[attr_name] = attr_val_1 + self.auth_context[attr_name] = attr_val_2 + self.assertEqual(attr_val_2, self.auth_context[attr_name]) - # Disable the trustor - trustor_update_ref = dict(enabled=False) - self.identity_api.update_user(self.user['id'], trustor_update_ref) - # Ensure validating a token for a disabled user fails - self.assertRaises(exception.TokenNotFound, - self.token_provider_api.validate_token, - trust_scoped_token) - def test_trust_scoped_token_invalid_after_changing_trustor_password(self): - trustee_user, trust = self._create_trust() - trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) - # Validate a trust scoped token - self._validate_token(trust_scoped_token) +class TestAuthSpecificData(test_v3.RestfulTestCase): - # Change trustor's password - trustor_update_ref = dict(password='Password1') - self.identity_api.update_user(self.user['id'], trustor_update_ref) - # Ensure updating trustor's password revokes existing user's tokens - self.assertRaises(exception.TokenNotFound, - self.token_provider_api.validate_token, - trust_scoped_token) + def test_get_catalog_project_scoped_token(self): + """Call ``GET /auth/catalog`` with a project-scoped token.""" + r = self.get('/auth/catalog') + self.assertValidCatalogResponse(r) - def test_trust_scoped_token_invalid_after_disabled_trustor_domain(self): - trustee_user, trust = self._create_trust() - trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) - # Validate a trust scoped token - self._validate_token(trust_scoped_token) + def test_get_catalog_domain_scoped_token(self): + """Call ``GET /auth/catalog`` with a domain-scoped token.""" + # grant a domain role to a user + self.put(path='/domains/%s/users/%s/roles/%s' % ( + self.domain['id'], self.user['id'], self.role['id'])) - # Disable trustor's domain - self.domain['enabled'] = False - self.resource_api.update_domain(self.domain['id'], self.domain) + self.get( + '/auth/catalog', + auth=self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + domain_id=self.domain['id']), + expected_status=http_client.FORBIDDEN) - trustor_update_ref = dict(password='Password1') - self.identity_api.update_user(self.user['id'], trustor_update_ref) - # Ensure updating trustor's password revokes existing user's tokens - self.assertRaises(exception.TokenNotFound, - self.token_provider_api.validate_token, - trust_scoped_token) + def test_get_catalog_unscoped_token(self): + """Call ``GET /auth/catalog`` with an unscoped token.""" + self.get( + '/auth/catalog', + auth=self.build_authentication_request( + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password']), + expected_status=http_client.FORBIDDEN) - def test_v2_validate_unscoped_token_returns_unauthorized(self): - """Test raised exception when validating unscoped token. + def test_get_catalog_no_token(self): + """Call ``GET /auth/catalog`` without a token.""" + self.get( + '/auth/catalog', + noauth=True, + expected_status=http_client.UNAUTHORIZED) - Test that validating an unscoped token in v2.0 of a v3 user of a - non-default domain returns unauthorized. - """ - unscoped_token = self._get_unscoped_token() - self.assertRaises(exception.Unauthorized, - self.token_provider_api.validate_v2_token, - unscoped_token) + def test_get_projects_project_scoped_token(self): + r = self.get('/auth/projects') + self.assertThat(r.json['projects'], matchers.HasLength(1)) + self.assertValidProjectListResponse(r) - def test_v2_validate_domain_scoped_token_returns_unauthorized(self): - """Test raised exception when validating a domain scoped token. + def test_get_domains_project_scoped_token(self): + self.put(path='/domains/%s/users/%s/roles/%s' % ( + self.domain['id'], self.user['id'], self.role['id'])) - Test that validating an domain scoped token in v2.0 - returns unauthorized. - """ + r = self.get('/auth/domains') + self.assertThat(r.json['domains'], matchers.HasLength(1)) + self.assertValidDomainListResponse(r) - # Grant user access to domain - self.assignment_api.create_grant(self.role['id'], - user_id=self.user['id'], - domain_id=self.domain['id']) - scoped_token = self._get_domain_scoped_token() - self.assertRaises(exception.Unauthorized, - self.token_provider_api.validate_v2_token, - scoped_token) +class TestTrustAuthPKITokenProvider(TrustAPIBehavior, TestTrustChain): + def config_overrides(self): + super(TestTrustAuthPKITokenProvider, self).config_overrides() + self.config_fixture.config(group='token', + provider='pki', + revoke_by_id=False) + self.config_fixture.config(group='trust', + enabled=True) - def test_v2_validate_trust_scoped_token(self): - """Test raised exception when validating a trust scoped token. - Test that validating an trust scoped token in v2.0 returns - unauthorized. - """ +class TestTrustAuthPKIZTokenProvider(TrustAPIBehavior, TestTrustChain): + def config_overrides(self): + super(TestTrustAuthPKIZTokenProvider, self).config_overrides() + self.config_fixture.config(group='token', + provider='pkiz', + revoke_by_id=False) + self.config_fixture.config(group='trust', + enabled=True) - trustee_user, trust = self._create_trust() - trust_scoped_token = self._get_trust_scoped_token(trustee_user, trust) - self.assertRaises(exception.Unauthorized, - self.token_provider_api.validate_v2_token, - trust_scoped_token) + +class TestTrustAuthFernetTokenProvider(TrustAPIBehavior, TestTrustChain): + def config_overrides(self): + super(TestTrustAuthFernetTokenProvider, self).config_overrides() + self.config_fixture.config(group='token', + provider='fernet', + revoke_by_id=False) + self.config_fixture.config(group='trust', + enabled=True) + self.useFixture(ksfixtures.KeyRepository(self.config_fixture)) class TestAuthFernetTokenProvider(TestAuth): @@ -4572,7 +4691,8 @@ class TestAuthFernetTokenProvider(TestAuth): self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, 'AUTH_TYPE': 'Negotiate'}) # Bind not current supported by Fernet, see bug 1433311. - self.v3_authenticate_token(auth_data, expected_status=501) + self.v3_create_token(auth_data, + expected_status=http_client.NOT_IMPLEMENTED) def test_v2_v3_bind_token_intermix(self): self.config_fixture.config(group='token', bind='kerberos') @@ -4587,7 +4707,7 @@ class TestAuthFernetTokenProvider(TestAuth): self.admin_request(path='/v2.0/tokens', method='POST', body=body, - expected_status=501) + expected_status=http_client.NOT_IMPLEMENTED) def test_auth_with_bind_token(self): self.config_fixture.config(group='token', bind=['kerberos']) @@ -4597,4 +4717,239 @@ class TestAuthFernetTokenProvider(TestAuth): self.admin_app.extra_environ.update({'REMOTE_USER': remote_user, 'AUTH_TYPE': 'Negotiate'}) # Bind not current supported by Fernet, see bug 1433311. - self.v3_authenticate_token(auth_data, expected_status=501) + self.v3_create_token(auth_data, + expected_status=http_client.NOT_IMPLEMENTED) + + +class TestAuthTOTP(test_v3.RestfulTestCase): + + def setUp(self): + super(TestAuthTOTP, self).setUp() + + ref = unit.new_totp_credential( + user_id=self.default_domain_user['id'], + project_id=self.default_domain_project['id']) + + self.secret = ref['blob'] + + r = self.post('/credentials', body={'credential': ref}) + self.assertValidCredentialResponse(r, ref) + + self.addCleanup(self.cleanup) + + def auth_plugin_config_override(self): + methods = ['totp', 'token', 'password'] + super(TestAuthTOTP, self).auth_plugin_config_override(methods) + + def _make_credentials(self, cred_type, count=1, user_id=None, + project_id=None, blob=None): + user_id = user_id or self.default_domain_user['id'] + project_id = project_id or self.default_domain_project['id'] + + creds = [] + for __ in range(count): + if cred_type == 'totp': + ref = unit.new_totp_credential( + user_id=user_id, project_id=project_id, blob=blob) + else: + ref = unit.new_credential_ref( + user_id=user_id, project_id=project_id) + resp = self.post('/credentials', body={'credential': ref}) + creds.append(resp.json['credential']) + return creds + + def _make_auth_data_by_id(self, passcode, user_id=None): + return self.build_authentication_request( + user_id=user_id or self.default_domain_user['id'], + passcode=passcode, + project_id=self.project['id']) + + def _make_auth_data_by_name(self, passcode, username, user_domain_id): + return self.build_authentication_request( + username=username, + user_domain_id=user_domain_id, + passcode=passcode, + project_id=self.project['id']) + + def cleanup(self): + totp_creds = self.credential_api.list_credentials_for_user( + self.default_domain_user['id'], type='totp') + + other_creds = self.credential_api.list_credentials_for_user( + self.default_domain_user['id'], type='other') + + for cred in itertools.chain(other_creds, totp_creds): + self.delete('/credentials/%s' % cred['id'], + expected_status=http_client.NO_CONTENT) + + def test_with_a_valid_passcode(self): + creds = self._make_credentials('totp') + secret = creds[-1]['blob'] + auth_data = self._make_auth_data_by_id( + totp._generate_totp_passcode(secret)) + + self.v3_create_token(auth_data, expected_status=http_client.CREATED) + + def test_with_an_invalid_passcode_and_user_credentials(self): + self._make_credentials('totp') + auth_data = self._make_auth_data_by_id('000000') + self.v3_create_token(auth_data, + expected_status=http_client.UNAUTHORIZED) + + def test_with_an_invalid_passcode_with_no_user_credentials(self): + auth_data = self._make_auth_data_by_id('000000') + self.v3_create_token(auth_data, + expected_status=http_client.UNAUTHORIZED) + + def test_with_a_corrupt_totp_credential(self): + self._make_credentials('totp', count=1, blob='0') + auth_data = self._make_auth_data_by_id('000000') + self.v3_create_token(auth_data, + expected_status=http_client.UNAUTHORIZED) + + def test_with_multiple_credentials(self): + self._make_credentials('other', 3) + creds = self._make_credentials('totp', count=3) + secret = creds[-1]['blob'] + + auth_data = self._make_auth_data_by_id( + totp._generate_totp_passcode(secret)) + self.v3_create_token(auth_data, expected_status=http_client.CREATED) + + def test_with_multiple_users(self): + # make some credentials for the existing user + self._make_credentials('totp', count=3) + + # create a new user and their credentials + user = unit.create_user(self.identity_api, domain_id=self.domain_id) + self.assignment_api.create_grant(self.role['id'], + user_id=user['id'], + project_id=self.project['id']) + creds = self._make_credentials('totp', count=1, user_id=user['id']) + secret = creds[-1]['blob'] + + # Stop the clock otherwise there is a chance of auth failure due to + # getting a different TOTP between the call here and the call in the + # auth plugin. + self.useFixture(fixture.TimeFixture()) + + auth_data = self._make_auth_data_by_id( + totp._generate_totp_passcode(secret), user_id=user['id']) + self.v3_create_token(auth_data, expected_status=http_client.CREATED) + + def test_with_multiple_users_and_invalid_credentials(self): + """Prevent logging in with someone else's credentials. + + It's very easy to forget to limit the credentials query by user. + Let's just test it for a sanity check. + """ + # make some credentials for the existing user + self._make_credentials('totp', count=3) + + # create a new user and their credentials + new_user = unit.create_user(self.identity_api, + domain_id=self.domain_id) + self.assignment_api.create_grant(self.role['id'], + user_id=new_user['id'], + project_id=self.project['id']) + user2_creds = self._make_credentials( + 'totp', count=1, user_id=new_user['id']) + + user_id = self.default_domain_user['id'] # user1 + secret = user2_creds[-1]['blob'] + + auth_data = self._make_auth_data_by_id( + totp._generate_totp_passcode(secret), user_id=user_id) + self.v3_create_token(auth_data, + expected_status=http_client.UNAUTHORIZED) + + def test_with_username_and_domain_id(self): + creds = self._make_credentials('totp') + secret = creds[-1]['blob'] + auth_data = self._make_auth_data_by_name( + totp._generate_totp_passcode(secret), + username=self.default_domain_user['name'], + user_domain_id=self.default_domain_user['domain_id']) + + self.v3_create_token(auth_data, expected_status=http_client.CREATED) + + +class TestFetchRevocationList(test_v3.RestfulTestCase): + """Test fetch token revocation list on the v3 Identity API.""" + + def config_overrides(self): + super(TestFetchRevocationList, self).config_overrides() + self.config_fixture.config(group='token', revoke_by_id=True) + + def test_ids_no_tokens(self): + # When there's no revoked tokens the response is an empty list, and + # the response is signed. + res = self.get('/auth/tokens/OS-PKI/revoked') + signed = res.json['signed'] + clear = cms.cms_verify(signed, CONF.signing.certfile, + CONF.signing.ca_certs) + payload = json.loads(clear) + self.assertEqual({'revoked': []}, payload) + + def test_ids_token(self): + # When there's a revoked token, it's in the response, and the response + # is signed. + token_res = self.v3_create_token( + self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project['id'])) + + token_id = token_res.headers.get('X-Subject-Token') + token_data = token_res.json['token'] + + self.delete('/auth/tokens', headers={'X-Subject-Token': token_id}) + + res = self.get('/auth/tokens/OS-PKI/revoked') + signed = res.json['signed'] + clear = cms.cms_verify(signed, CONF.signing.certfile, + CONF.signing.ca_certs) + payload = json.loads(clear) + + def truncate(ts_str): + return ts_str[:19] + 'Z' # 2016-01-21T15:53:52 == 19 chars. + + exp_token_revoke_data = { + 'id': token_id, + 'audit_id': token_data['audit_ids'][0], + 'expires': truncate(token_data['expires_at']), + } + + self.assertEqual({'revoked': [exp_token_revoke_data]}, payload) + + def test_audit_id_only_no_tokens(self): + # When there's no revoked tokens and ?audit_id_only is used, the + # response is an empty list and is not signed. + res = self.get('/auth/tokens/OS-PKI/revoked?audit_id_only') + self.assertEqual({'revoked': []}, res.json) + + def test_audit_id_only_token(self): + # When there's a revoked token and ?audit_id_only is used, the + # response contains the audit_id of the token and is not signed. + token_res = self.v3_create_token( + self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + project_id=self.project['id'])) + + token_id = token_res.headers.get('X-Subject-Token') + token_data = token_res.json['token'] + + self.delete('/auth/tokens', headers={'X-Subject-Token': token_id}) + + res = self.get('/auth/tokens/OS-PKI/revoked?audit_id_only') + + def truncate(ts_str): + return ts_str[:19] + 'Z' # 2016-01-21T15:53:52 == 19 chars. + + exp_token_revoke_data = { + 'audit_id': token_data['audit_ids'][0], + 'expires': truncate(token_data['expires_at']), + } + + self.assertEqual({'revoked': [exp_token_revoke_data]}, res.json) diff --git a/keystone-moon/keystone/tests/unit/test_v3_catalog.py b/keystone-moon/keystone/tests/unit/test_v3_catalog.py index c536169a..2eb9db14 100644 --- a/keystone-moon/keystone/tests/unit/test_v3_catalog.py +++ b/keystone-moon/keystone/tests/unit/test_v3_catalog.py @@ -31,12 +31,12 @@ class CatalogTestCase(test_v3.RestfulTestCase): def test_create_region_with_id(self): """Call ``PUT /regions/{region_id}`` w/o an ID in the request body.""" - ref = self.new_region_ref() + ref = unit.new_region_ref() region_id = ref.pop('id') r = self.put( '/regions/%s' % region_id, body={'region': ref}, - expected_status=201) + expected_status=http_client.CREATED) self.assertValidRegionResponse(r, ref) # Double-check that the region ID was kept as-is and not # populated with a UUID, as is the case with POST /v3/regions @@ -44,12 +44,12 @@ class CatalogTestCase(test_v3.RestfulTestCase): def test_create_region_with_matching_ids(self): """Call ``PUT /regions/{region_id}`` with an ID in the request body.""" - ref = self.new_region_ref() + ref = unit.new_region_ref() region_id = ref['id'] r = self.put( '/regions/%s' % region_id, body={'region': ref}, - expected_status=201) + expected_status=http_client.CREATED) self.assertValidRegionResponse(r, ref) # Double-check that the region ID was kept as-is and not # populated with a UUID, as is the case with POST /v3/regions @@ -60,16 +60,16 @@ class CatalogTestCase(test_v3.RestfulTestCase): ref = dict(description="my region") self.put( '/regions/myregion', - body={'region': ref}, expected_status=201) + body={'region': ref}, expected_status=http_client.CREATED) # Create region again with duplicate id self.put( '/regions/myregion', - body={'region': ref}, expected_status=409) + body={'region': ref}, expected_status=http_client.CONFLICT) def test_create_region(self): """Call ``POST /regions`` with an ID in the request body.""" # the ref will have an ID defined on it - ref = self.new_region_ref() + ref = unit.new_region_ref() r = self.post( '/regions', body={'region': ref}) @@ -83,39 +83,30 @@ class CatalogTestCase(test_v3.RestfulTestCase): def test_create_region_with_empty_id(self): """Call ``POST /regions`` with an empty ID in the request body.""" - ref = self.new_region_ref() - ref['id'] = '' + ref = unit.new_region_ref(id='') - r = self.post( - '/regions', - body={'region': ref}, expected_status=201) + r = self.post('/regions', body={'region': ref}) self.assertValidRegionResponse(r, ref) self.assertNotEmpty(r.result['region'].get('id')) def test_create_region_without_id(self): """Call ``POST /regions`` without an ID in the request body.""" - ref = self.new_region_ref() + ref = unit.new_region_ref() # instead of defining the ID ourselves... del ref['id'] # let the service define the ID - r = self.post( - '/regions', - body={'region': ref}, - expected_status=201) + r = self.post('/regions', body={'region': ref}) self.assertValidRegionResponse(r, ref) def test_create_region_without_description(self): """Call ``POST /regions`` without description in the request body.""" - ref = self.new_region_ref() + ref = unit.new_region_ref(description=None) del ref['description'] - r = self.post( - '/regions', - body={'region': ref}, - expected_status=201) + r = self.post('/regions', body={'region': ref}) # Create the description in the reference to compare to since the # response should now have a description, even though we didn't send # it with the original reference. @@ -123,51 +114,34 @@ class CatalogTestCase(test_v3.RestfulTestCase): self.assertValidRegionResponse(r, ref) def test_create_regions_with_same_description_string(self): - """Call ``POST /regions`` with same description in the request bodies. - """ + """Call ``POST /regions`` with duplicate descriptions.""" # NOTE(lbragstad): Make sure we can create two regions that have the # same description. - ref1 = self.new_region_ref() - ref2 = self.new_region_ref() - region_desc = 'Some Region Description' - ref1['description'] = region_desc - ref2['description'] = region_desc + ref1 = unit.new_region_ref(description=region_desc) + ref2 = unit.new_region_ref(description=region_desc) - resp1 = self.post( - '/regions', - body={'region': ref1}, - expected_status=201) + resp1 = self.post('/regions', body={'region': ref1}) self.assertValidRegionResponse(resp1, ref1) - resp2 = self.post( - '/regions', - body={'region': ref2}, - expected_status=201) + resp2 = self.post('/regions', body={'region': ref2}) self.assertValidRegionResponse(resp2, ref2) def test_create_regions_without_descriptions(self): - """Call ``POST /regions`` with no description in the request bodies. - """ + """Call ``POST /regions`` with no description.""" # NOTE(lbragstad): Make sure we can create two regions that have # no description in the request body. The description should be # populated by Catalog Manager. - ref1 = self.new_region_ref() - ref2 = self.new_region_ref() + ref1 = unit.new_region_ref() + ref2 = unit.new_region_ref() del ref1['description'] ref2['description'] = None - resp1 = self.post( - '/regions', - body={'region': ref1}, - expected_status=201) + resp1 = self.post('/regions', body={'region': ref1}) - resp2 = self.post( - '/regions', - body={'region': ref2}, - expected_status=201) + resp2 = self.post('/regions', body={'region': ref2}) # Create the descriptions in the references to compare to since the # responses should now have descriptions, even though we didn't send # a description with the original references. @@ -179,7 +153,7 @@ class CatalogTestCase(test_v3.RestfulTestCase): def test_create_region_with_conflicting_ids(self): """Call ``PUT /regions/{region_id}`` with conflicting region IDs.""" # the region ref is created with an ID - ref = self.new_region_ref() + ref = unit.new_region_ref() # but instead of using that ID, make up a new, conflicting one self.put( @@ -193,8 +167,7 @@ class CatalogTestCase(test_v3.RestfulTestCase): self.assertValidRegionListResponse(r, ref=self.region) def _create_region_with_parent_id(self, parent_id=None): - ref = self.new_region_ref() - ref['parent_region_id'] = parent_id + ref = unit.new_region_ref(parent_region_id=parent_id) return self.post( '/regions', body={'region': ref}) @@ -220,7 +193,7 @@ class CatalogTestCase(test_v3.RestfulTestCase): def test_update_region(self): """Call ``PATCH /regions/{region_id}``.""" - region = self.new_region_ref() + region = unit.new_region_ref() del region['id'] r = self.patch('/regions/%(region_id)s' % { 'region_id': self.region_id}, @@ -229,18 +202,16 @@ class CatalogTestCase(test_v3.RestfulTestCase): def test_update_region_without_description_keeps_original(self): """Call ``PATCH /regions/{region_id}``.""" - region_ref = self.new_region_ref() + region_ref = unit.new_region_ref() - resp = self.post('/regions', body={'region': region_ref}, - expected_status=201) + resp = self.post('/regions', body={'region': region_ref}) region_updates = { # update with something that's not the description 'parent_region_id': self.region_id, } resp = self.patch('/regions/%s' % region_ref['id'], - body={'region': region_updates}, - expected_status=200) + body={'region': region_updates}) # NOTE(dstanek): Keystone should keep the original description. self.assertEqual(region_ref['description'], @@ -248,9 +219,8 @@ class CatalogTestCase(test_v3.RestfulTestCase): def test_update_region_with_null_description(self): """Call ``PATCH /regions/{region_id}``.""" - region = self.new_region_ref() + region = unit.new_region_ref(description=None) del region['id'] - region['description'] = None r = self.patch('/regions/%(region_id)s' % { 'region_id': self.region_id}, body={'region': region}) @@ -262,8 +232,7 @@ class CatalogTestCase(test_v3.RestfulTestCase): def test_delete_region(self): """Call ``DELETE /regions/{region_id}``.""" - - ref = self.new_region_ref() + ref = unit.new_region_ref() r = self.post( '/regions', body={'region': ref}) @@ -276,7 +245,7 @@ class CatalogTestCase(test_v3.RestfulTestCase): def test_create_service(self): """Call ``POST /services``.""" - ref = self.new_service_ref() + ref = unit.new_service_ref() r = self.post( '/services', body={'service': ref}) @@ -284,7 +253,7 @@ class CatalogTestCase(test_v3.RestfulTestCase): def test_create_service_no_name(self): """Call ``POST /services``.""" - ref = self.new_service_ref() + ref = unit.new_service_ref() del ref['name'] r = self.post( '/services', @@ -294,7 +263,7 @@ class CatalogTestCase(test_v3.RestfulTestCase): def test_create_service_no_enabled(self): """Call ``POST /services``.""" - ref = self.new_service_ref() + ref = unit.new_service_ref() del ref['enabled'] r = self.post( '/services', @@ -305,8 +274,7 @@ class CatalogTestCase(test_v3.RestfulTestCase): def test_create_service_enabled_false(self): """Call ``POST /services``.""" - ref = self.new_service_ref() - ref['enabled'] = False + ref = unit.new_service_ref(enabled=False) r = self.post( '/services', body={'service': ref}) @@ -315,8 +283,7 @@ class CatalogTestCase(test_v3.RestfulTestCase): def test_create_service_enabled_true(self): """Call ``POST /services``.""" - ref = self.new_service_ref() - ref['enabled'] = True + ref = unit.new_service_ref(enabled=True) r = self.post( '/services', body={'service': ref}) @@ -325,22 +292,19 @@ class CatalogTestCase(test_v3.RestfulTestCase): def test_create_service_enabled_str_true(self): """Call ``POST /services``.""" - ref = self.new_service_ref() - ref['enabled'] = 'True' + ref = unit.new_service_ref(enabled='True') self.post('/services', body={'service': ref}, expected_status=http_client.BAD_REQUEST) def test_create_service_enabled_str_false(self): """Call ``POST /services``.""" - ref = self.new_service_ref() - ref['enabled'] = 'False' + ref = unit.new_service_ref(enabled='False') self.post('/services', body={'service': ref}, expected_status=http_client.BAD_REQUEST) def test_create_service_enabled_str_random(self): """Call ``POST /services``.""" - ref = self.new_service_ref() - ref['enabled'] = 'puppies' + ref = unit.new_service_ref(enabled='puppies') self.post('/services', body={'service': ref}, expected_status=http_client.BAD_REQUEST) @@ -350,8 +314,7 @@ class CatalogTestCase(test_v3.RestfulTestCase): self.assertValidServiceListResponse(r, ref=self.service) def _create_random_service(self): - ref = self.new_service_ref() - ref['enabled'] = True + ref = unit.new_service_ref() response = self.post( '/services', body={'service': ref}) @@ -399,7 +362,7 @@ class CatalogTestCase(test_v3.RestfulTestCase): def test_update_service(self): """Call ``PATCH /services/{service_id}``.""" - service = self.new_service_ref() + service = unit.new_service_ref() del service['id'] r = self.patch('/services/%(service_id)s' % { 'service_id': self.service_id}, @@ -423,7 +386,7 @@ class CatalogTestCase(test_v3.RestfulTestCase): region = self._create_region_with_parent_id( parent_id=parent_region_id) service = self._create_random_service() - ref = self.new_endpoint_ref( + ref = unit.new_endpoint_ref( service_id=service['id'], interface=interface, region_id=region.result['region']['id']) @@ -547,87 +510,84 @@ class CatalogTestCase(test_v3.RestfulTestCase): def test_create_endpoint_no_enabled(self): """Call ``POST /endpoints``.""" - ref = self.new_endpoint_ref(service_id=self.service_id) - r = self.post( - '/endpoints', - body={'endpoint': ref}) + ref = unit.new_endpoint_ref(service_id=self.service_id, + interface='public', + region_id=self.region_id) + r = self.post('/endpoints', body={'endpoint': ref}) ref['enabled'] = True self.assertValidEndpointResponse(r, ref) def test_create_endpoint_enabled_true(self): """Call ``POST /endpoints`` with enabled: true.""" - ref = self.new_endpoint_ref(service_id=self.service_id, + ref = unit.new_endpoint_ref(service_id=self.service_id, + interface='public', + region_id=self.region_id, enabled=True) - r = self.post( - '/endpoints', - body={'endpoint': ref}) + r = self.post('/endpoints', body={'endpoint': ref}) self.assertValidEndpointResponse(r, ref) def test_create_endpoint_enabled_false(self): """Call ``POST /endpoints`` with enabled: false.""" - ref = self.new_endpoint_ref(service_id=self.service_id, + ref = unit.new_endpoint_ref(service_id=self.service_id, + interface='public', + region_id=self.region_id, enabled=False) - r = self.post( - '/endpoints', - body={'endpoint': ref}) + r = self.post('/endpoints', body={'endpoint': ref}) self.assertValidEndpointResponse(r, ref) def test_create_endpoint_enabled_str_true(self): """Call ``POST /endpoints`` with enabled: 'True'.""" - ref = self.new_endpoint_ref(service_id=self.service_id, + ref = unit.new_endpoint_ref(service_id=self.service_id, + interface='public', + region_id=self.region_id, enabled='True') - self.post( - '/endpoints', - body={'endpoint': ref}, - expected_status=http_client.BAD_REQUEST) + self.post('/endpoints', body={'endpoint': ref}, + expected_status=http_client.BAD_REQUEST) def test_create_endpoint_enabled_str_false(self): """Call ``POST /endpoints`` with enabled: 'False'.""" - ref = self.new_endpoint_ref(service_id=self.service_id, + ref = unit.new_endpoint_ref(service_id=self.service_id, + interface='public', + region_id=self.region_id, enabled='False') - self.post( - '/endpoints', - body={'endpoint': ref}, - expected_status=http_client.BAD_REQUEST) + self.post('/endpoints', body={'endpoint': ref}, + expected_status=http_client.BAD_REQUEST) def test_create_endpoint_enabled_str_random(self): """Call ``POST /endpoints`` with enabled: 'puppies'.""" - ref = self.new_endpoint_ref(service_id=self.service_id, + ref = unit.new_endpoint_ref(service_id=self.service_id, + interface='public', + region_id=self.region_id, enabled='puppies') - self.post( - '/endpoints', - body={'endpoint': ref}, - expected_status=http_client.BAD_REQUEST) + self.post('/endpoints', body={'endpoint': ref}, + expected_status=http_client.BAD_REQUEST) def test_create_endpoint_with_invalid_region_id(self): """Call ``POST /endpoints``.""" - ref = self.new_endpoint_ref(service_id=self.service_id) - ref["region_id"] = uuid.uuid4().hex + ref = unit.new_endpoint_ref(service_id=self.service_id) self.post('/endpoints', body={'endpoint': ref}, expected_status=http_client.BAD_REQUEST) def test_create_endpoint_with_region(self): - """EndpointV3 creates the region before creating the endpoint, if - endpoint is provided with 'region' and no 'region_id' + """EndpointV3 creates the region before creating the endpoint. + + This occurs when endpoint is provided with 'region' and no 'region_id'. """ - ref = self.new_endpoint_ref(service_id=self.service_id) - ref["region"] = uuid.uuid4().hex - ref.pop('region_id') - self.post('/endpoints', body={'endpoint': ref}, expected_status=201) + ref = unit.new_endpoint_ref_with_region(service_id=self.service_id, + region=uuid.uuid4().hex) + self.post('/endpoints', body={'endpoint': ref}) # Make sure the region is created - self.get('/regions/%(region_id)s' % { - 'region_id': ref["region"]}) + self.get('/regions/%(region_id)s' % {'region_id': ref["region"]}) def test_create_endpoint_with_no_region(self): """EndpointV3 allows to creates the endpoint without region.""" - ref = self.new_endpoint_ref(service_id=self.service_id) - ref.pop('region_id') - self.post('/endpoints', body={'endpoint': ref}, expected_status=201) + ref = unit.new_endpoint_ref(service_id=self.service_id, region_id=None) + del ref['region_id'] # cannot just be None, it needs to not exist + self.post('/endpoints', body={'endpoint': ref}) def test_create_endpoint_with_empty_url(self): """Call ``POST /endpoints``.""" - ref = self.new_endpoint_ref(service_id=self.service_id) - ref["url"] = '' + ref = unit.new_endpoint_ref(service_id=self.service_id, url='') self.post('/endpoints', body={'endpoint': ref}, expected_status=http_client.BAD_REQUEST) @@ -640,7 +600,9 @@ class CatalogTestCase(test_v3.RestfulTestCase): def test_update_endpoint(self): """Call ``PATCH /endpoints/{endpoint_id}``.""" - ref = self.new_endpoint_ref(service_id=self.service_id) + ref = unit.new_endpoint_ref(service_id=self.service_id, + interface='public', + region_id=self.region_id) del ref['id'] r = self.patch( '/endpoints/%(endpoint_id)s' % { @@ -704,13 +666,12 @@ class CatalogTestCase(test_v3.RestfulTestCase): 'endpoint_id': self.endpoint_id}) # create a v3 endpoint ref, and then tweak it back to a v2-style ref - ref = self.new_endpoint_ref(service_id=self.service['id']) + ref = unit.new_endpoint_ref_with_region(service_id=self.service['id'], + region=uuid.uuid4().hex, + internalurl=None) del ref['id'] del ref['interface'] ref['publicurl'] = ref.pop('url') - ref['internalurl'] = None - ref['region'] = ref['region_id'] - del ref['region_id'] # don't set adminurl to ensure it's absence is handled like internalurl # create the endpoint on v2 (using a v3 token) @@ -751,15 +712,16 @@ class CatalogTestCase(test_v3.RestfulTestCase): self.assertEqual(endpoint_v2['region'], endpoint_v3['region_id']) def test_deleting_endpoint_with_space_in_url(self): - # create a v3 endpoint ref - ref = self.new_endpoint_ref(service_id=self.service['id']) - # add a space to all urls (intentional "i d" to test bug) url_with_space = "http://127.0.0.1:8774 /v1.1/\$(tenant_i d)s" - ref['publicurl'] = url_with_space - ref['internalurl'] = url_with_space - ref['adminurl'] = url_with_space - ref['url'] = url_with_space + + # create a v3 endpoint ref + ref = unit.new_endpoint_ref(service_id=self.service['id'], + region_id=None, + publicurl=url_with_space, + internalurl=url_with_space, + adminurl=url_with_space, + url=url_with_space) # add the endpoint to the database self.catalog_api.create_endpoint(ref['id'], ref) @@ -767,7 +729,7 @@ class CatalogTestCase(test_v3.RestfulTestCase): # delete the endpoint self.delete('/endpoints/%s' % ref['id']) - # make sure it's deleted (GET should return 404) + # make sure it's deleted (GET should return Not Found) self.get('/endpoints/%s' % ref['id'], expected_status=http_client.NOT_FOUND) @@ -776,15 +738,24 @@ class CatalogTestCase(test_v3.RestfulTestCase): # list one valid url is enough, no need to list too much valid_url = 'http://127.0.0.1:8774/v1.1/$(tenant_id)s' - ref = self.new_endpoint_ref(self.service_id) - ref['url'] = valid_url - self.post('/endpoints', - body={'endpoint': ref}, - expected_status=201) + ref = unit.new_endpoint_ref(self.service_id, + interface='public', + region_id=self.region_id, + url=valid_url) + self.post('/endpoints', body={'endpoint': ref}) + + def test_endpoint_create_with_valid_url_project_id(self): + """Create endpoint with valid url should be tested,too.""" + valid_url = 'http://127.0.0.1:8774/v1.1/$(project_id)s' + + ref = unit.new_endpoint_ref(self.service_id, + interface='public', + region_id=self.region_id, + url=valid_url) + self.post('/endpoints', body={'endpoint': ref}) def test_endpoint_create_with_invalid_url(self): - """Test the invalid cases: substitutions is not exactly right. - """ + """Test the invalid cases: substitutions is not exactly right.""" invalid_urls = [ # using a substitution that is not whitelisted - KeyError 'http://127.0.0.1:8774/v1.1/$(nonexistent)s', @@ -799,7 +770,7 @@ class CatalogTestCase(test_v3.RestfulTestCase): 'http://127.0.0.1:8774/v1.1/$(admin_url)d', ] - ref = self.new_endpoint_ref(self.service_id) + ref = unit.new_endpoint_ref(self.service_id) for invalid_url in invalid_urls: ref['url'] = invalid_url @@ -809,37 +780,30 @@ class CatalogTestCase(test_v3.RestfulTestCase): class TestCatalogAPISQL(unit.TestCase): - """Tests for the catalog Manager against the SQL backend. - - """ + """Tests for the catalog Manager against the SQL backend.""" def setUp(self): super(TestCatalogAPISQL, self).setUp() self.useFixture(database.Database()) self.catalog_api = catalog.Manager() - self.service_id = uuid.uuid4().hex - service = {'id': self.service_id, 'name': uuid.uuid4().hex} + service = unit.new_service_ref() + self.service_id = service['id'] self.catalog_api.create_service(self.service_id, service) - endpoint = self.new_endpoint_ref(service_id=self.service_id) + self.create_endpoint(service_id=self.service_id) + + def create_endpoint(self, service_id, **kwargs): + endpoint = unit.new_endpoint_ref(service_id=service_id, + region_id=None, **kwargs) + self.catalog_api.create_endpoint(endpoint['id'], endpoint) + return endpoint def config_overrides(self): super(TestCatalogAPISQL, self).config_overrides() self.config_fixture.config(group='catalog', driver='sql') - def new_endpoint_ref(self, service_id): - return { - 'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex, - 'description': uuid.uuid4().hex, - 'interface': uuid.uuid4().hex[:8], - 'service_id': service_id, - 'url': uuid.uuid4().hex, - 'region': uuid.uuid4().hex, - } - def test_get_catalog_ignores_endpoints_with_invalid_urls(self): user_id = uuid.uuid4().hex tenant_id = uuid.uuid4().hex @@ -851,14 +815,12 @@ class TestCatalogAPISQL(unit.TestCase): self.assertEqual(1, len(self.catalog_api.list_endpoints())) # create a new, invalid endpoint - malformed type declaration - ref = self.new_endpoint_ref(self.service_id) - ref['url'] = 'http://keystone/%(tenant_id)' - self.catalog_api.create_endpoint(ref['id'], ref) + self.create_endpoint(self.service_id, + url='http://keystone/%(tenant_id)') # create a new, invalid endpoint - nonexistent key - ref = self.new_endpoint_ref(self.service_id) - ref['url'] = 'http://keystone/%(you_wont_find_me)s' - self.catalog_api.create_endpoint(ref['id'], ref) + self.create_endpoint(self.service_id, + url='http://keystone/%(you_wont_find_me)s') # verify that the invalid endpoints don't appear in the catalog catalog = self.catalog_api.get_v3_catalog(user_id, tenant_id) @@ -867,9 +829,8 @@ class TestCatalogAPISQL(unit.TestCase): self.assertEqual(3, len(self.catalog_api.list_endpoints())) # create another valid endpoint - tenant_id will be replaced - ref = self.new_endpoint_ref(self.service_id) - ref['url'] = 'http://keystone/%(tenant_id)s' - self.catalog_api.create_endpoint(ref['id'], ref) + self.create_endpoint(self.service_id, + url='http://keystone/%(tenant_id)s') # there are two valid endpoints, positive check catalog = self.catalog_api.get_v3_catalog(user_id, tenant_id) @@ -877,7 +838,8 @@ class TestCatalogAPISQL(unit.TestCase): # If the URL has no 'tenant_id' to substitute, we will skip the # endpoint which contains this kind of URL, negative check. - catalog = self.catalog_api.get_v3_catalog(user_id, tenant_id=None) + tenant_id = None + catalog = self.catalog_api.get_v3_catalog(user_id, tenant_id) self.assertThat(catalog[0]['endpoints'], matchers.HasLength(1)) def test_get_catalog_always_returns_service_name(self): @@ -885,23 +847,15 @@ class TestCatalogAPISQL(unit.TestCase): tenant_id = uuid.uuid4().hex # create a service, with a name - named_svc = { - 'id': uuid.uuid4().hex, - 'type': uuid.uuid4().hex, - 'name': uuid.uuid4().hex, - } + named_svc = unit.new_service_ref() self.catalog_api.create_service(named_svc['id'], named_svc) - endpoint = self.new_endpoint_ref(service_id=named_svc['id']) - self.catalog_api.create_endpoint(endpoint['id'], endpoint) + self.create_endpoint(service_id=named_svc['id']) # create a service, with no name - unnamed_svc = { - 'id': uuid.uuid4().hex, - 'type': uuid.uuid4().hex - } + unnamed_svc = unit.new_service_ref(name=None) + del unnamed_svc['name'] self.catalog_api.create_service(unnamed_svc['id'], unnamed_svc) - endpoint = self.new_endpoint_ref(service_id=unnamed_svc['id']) - self.catalog_api.create_endpoint(endpoint['id'], endpoint) + self.create_endpoint(service_id=unnamed_svc['id']) catalog = self.catalog_api.get_v3_catalog(user_id, tenant_id) @@ -917,9 +871,7 @@ class TestCatalogAPISQL(unit.TestCase): # TODO(dstanek): this needs refactoring with the test above, but we are in a # crunch so that will happen in a future patch. class TestCatalogAPISQLRegions(unit.TestCase): - """Tests for the catalog Manager against the SQL backend. - - """ + """Tests for the catalog Manager against the SQL backend.""" def setUp(self): super(TestCatalogAPISQLRegions, self).setUp() @@ -930,23 +882,13 @@ class TestCatalogAPISQLRegions(unit.TestCase): super(TestCatalogAPISQLRegions, self).config_overrides() self.config_fixture.config(group='catalog', driver='sql') - def new_endpoint_ref(self, service_id): - return { - 'id': uuid.uuid4().hex, - 'name': uuid.uuid4().hex, - 'description': uuid.uuid4().hex, - 'interface': uuid.uuid4().hex[:8], - 'service_id': service_id, - 'url': uuid.uuid4().hex, - 'region_id': uuid.uuid4().hex, - } - def test_get_catalog_returns_proper_endpoints_with_no_region(self): - service_id = uuid.uuid4().hex - service = {'id': service_id, 'name': uuid.uuid4().hex} + service = unit.new_service_ref() + service_id = service['id'] self.catalog_api.create_service(service_id, service) - endpoint = self.new_endpoint_ref(service_id=service_id) + endpoint = unit.new_endpoint_ref(service_id=service_id, + region_id=None) del endpoint['region_id'] self.catalog_api.create_endpoint(endpoint['id'], endpoint) @@ -958,12 +900,13 @@ class TestCatalogAPISQLRegions(unit.TestCase): catalog[0]['endpoints'][0], ref=endpoint) def test_get_catalog_returns_proper_endpoints_with_region(self): - service_id = uuid.uuid4().hex - service = {'id': service_id, 'name': uuid.uuid4().hex} + service = unit.new_service_ref() + service_id = service['id'] self.catalog_api.create_service(service_id, service) - endpoint = self.new_endpoint_ref(service_id=service_id) - self.catalog_api.create_region({'id': endpoint['region_id']}) + endpoint = unit.new_endpoint_ref(service_id=service_id) + region = unit.new_region_ref(id=endpoint['region_id']) + self.catalog_api.create_region(region) self.catalog_api.create_endpoint(endpoint['id'], endpoint) endpoint = self.catalog_api.get_endpoint(endpoint['id']) diff --git a/keystone-moon/keystone/tests/unit/test_v3_credential.py b/keystone-moon/keystone/tests/unit/test_v3_credential.py index dd8cf2dd..07995f19 100644 --- a/keystone-moon/keystone/tests/unit/test_v3_credential.py +++ b/keystone-moon/keystone/tests/unit/test_v3_credential.py @@ -21,49 +21,46 @@ from oslo_config import cfg from six.moves import http_client from testtools import matchers +from keystone.common import utils +from keystone.contrib.ec2 import controllers from keystone import exception +from keystone.tests import unit from keystone.tests.unit import test_v3 CONF = cfg.CONF +CRED_TYPE_EC2 = controllers.CRED_TYPE_EC2 class CredentialBaseTestCase(test_v3.RestfulTestCase): def _create_dict_blob_credential(self): - blob = {"access": uuid.uuid4().hex, - "secret": uuid.uuid4().hex} - credential_id = hashlib.sha256(blob['access']).hexdigest() - credential = self.new_credential_ref( - user_id=self.user['id'], - project_id=self.project_id) - credential['id'] = credential_id + blob, credential = unit.new_ec2_credential(user_id=self.user['id'], + project_id=self.project_id) # Store the blob as a dict *not* JSON ref bug #1259584 # This means we can test the dict->json workaround, added # as part of the bugfix for backwards compatibility works. credential['blob'] = blob - credential['type'] = 'ec2' + credential_id = credential['id'] + # Create direct via the DB API to avoid validation failure - self.credential_api.create_credential( - credential_id, - credential) - expected_blob = json.dumps(blob) - return expected_blob, credential_id + self.credential_api.create_credential(credential_id, credential) + + return json.dumps(blob), credential_id class CredentialTestCase(CredentialBaseTestCase): """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 = unit.new_credential_ref(user_id=self.user['id'], + project_id=self.project_id) + self.credential_api.create_credential( - self.credential_id, + self.credential['id'], self.credential) def test_credential_api_delete_credentials_for_project(self): @@ -72,7 +69,7 @@ class CredentialTestCase(CredentialBaseTestCase): # once we delete all credentials for self.project_id self.assertRaises(exception.CredentialNotFound, self.credential_api.get_credential, - credential_id=self.credential_id) + credential_id=self.credential['id']) def test_credential_api_delete_credentials_for_user(self): self.credential_api.delete_credentials_for_user(self.user_id) @@ -80,7 +77,7 @@ class CredentialTestCase(CredentialBaseTestCase): # once we delete all credentials for self.user_id self.assertRaises(exception.CredentialNotFound, self.credential_api.get_credential, - credential_id=self.credential_id) + credential_id=self.credential['id']) def test_list_credentials(self): """Call ``GET /credentials``.""" @@ -89,10 +86,8 @@ class CredentialTestCase(CredentialBaseTestCase): def test_list_credentials_filtered_by_user_id(self): """Call ``GET /credentials?user_id={user_id}``.""" - credential = self.new_credential_ref( - user_id=uuid.uuid4().hex) - self.credential_api.create_credential( - credential['id'], credential) + credential = unit.new_credential_ref(user_id=uuid.uuid4().hex) + self.credential_api.create_credential(credential['id'], credential) r = self.get('/credentials?user_id=%s' % self.user['id']) self.assertValidCredentialListResponse(r, ref=self.credential) @@ -103,9 +98,9 @@ class CredentialTestCase(CredentialBaseTestCase): """Call ``GET /credentials?type={type}``.""" # The type ec2 was chosen, instead of a random string, # because the type must be in the list of supported types - ec2_credential = self.new_credential_ref(user_id=uuid.uuid4().hex, + ec2_credential = unit.new_credential_ref(user_id=uuid.uuid4().hex, project_id=self.project_id, - cred_type='ec2') + type=CRED_TYPE_EC2) ec2_resp = self.credential_api.create_credential( ec2_credential['id'], ec2_credential) @@ -123,8 +118,8 @@ class CredentialTestCase(CredentialBaseTestCase): cred_ec2 = r_ec2.result['credentials'][0] self.assertValidCredentialListResponse(r_ec2, ref=ec2_resp) - self.assertEqual('ec2', cred_ec2['type']) - self.assertEqual(cred_ec2['id'], ec2_credential['id']) + self.assertEqual(CRED_TYPE_EC2, cred_ec2['type']) + self.assertEqual(ec2_credential['id'], cred_ec2['id']) def test_list_credentials_filtered_by_type_and_user_id(self): """Call ``GET /credentials?user_id={user_id}&type={type}``.""" @@ -132,12 +127,10 @@ class CredentialTestCase(CredentialBaseTestCase): user2_id = uuid.uuid4().hex # Creating credentials for two different users - credential_user1_ec2 = self.new_credential_ref( - user_id=user1_id, cred_type='ec2') - credential_user1_cert = self.new_credential_ref( - user_id=user1_id) - credential_user2_cert = self.new_credential_ref( - user_id=user2_id) + credential_user1_ec2 = unit.new_credential_ref(user_id=user1_id, + type=CRED_TYPE_EC2) + credential_user1_cert = unit.new_credential_ref(user_id=user1_id) + credential_user2_cert = unit.new_credential_ref(user_id=user2_id) self.credential_api.create_credential( credential_user1_ec2['id'], credential_user1_ec2) @@ -150,12 +143,12 @@ class CredentialTestCase(CredentialBaseTestCase): self.assertValidCredentialListResponse(r, ref=credential_user1_ec2) self.assertThat(r.result['credentials'], matchers.HasLength(1)) cred = r.result['credentials'][0] - self.assertEqual('ec2', cred['type']) + self.assertEqual(CRED_TYPE_EC2, cred['type']) self.assertEqual(user1_id, cred['user_id']) def test_create_credential(self): """Call ``POST /credentials``.""" - ref = self.new_credential_ref(user_id=self.user['id']) + ref = unit.new_credential_ref(user_id=self.user['id']) r = self.post( '/credentials', body={'credential': ref}) @@ -165,18 +158,17 @@ class CredentialTestCase(CredentialBaseTestCase): """Call ``GET /credentials/{credential_id}``.""" r = self.get( '/credentials/%(credential_id)s' % { - 'credential_id': self.credential_id}) + 'credential_id': self.credential['id']}) self.assertValidCredentialResponse(r, self.credential) def test_update_credential(self): """Call ``PATCH /credentials/{credential_id}``.""" - ref = self.new_credential_ref( - user_id=self.user['id'], - project_id=self.project_id) + ref = unit.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}, + 'credential_id': self.credential['id']}, body={'credential': ref}) self.assertValidCredentialResponse(r, ref) @@ -184,29 +176,24 @@ class CredentialTestCase(CredentialBaseTestCase): """Call ``DELETE /credentials/{credential_id}``.""" self.delete( '/credentials/%(credential_id)s' % { - 'credential_id': self.credential_id}) + 'credential_id': self.credential['id']}) def test_create_ec2_credential(self): """Call ``POST /credentials`` for creating ec2 credential.""" - ref = self.new_credential_ref(user_id=self.user['id'], - project_id=self.project_id) - blob = {"access": uuid.uuid4().hex, - "secret": uuid.uuid4().hex} - ref['blob'] = json.dumps(blob) - ref['type'] = 'ec2' - r = self.post( - '/credentials', - body={'credential': ref}) + blob, ref = unit.new_ec2_credential(user_id=self.user['id'], + project_id=self.project_id) + r = self.post('/credentials', body={'credential': ref}) self.assertValidCredentialResponse(r, ref) # Assert credential id is same as hash of access key id for # ec2 credentials - self.assertEqual(r.result['credential']['id'], - hashlib.sha256(blob['access']).hexdigest()) + access = blob['access'].encode('utf-8') + self.assertEqual(hashlib.sha256(access).hexdigest(), + r.result['credential']['id']) # Create second ec2 credential with the same access key id and check # for conflict. self.post( '/credentials', - body={'credential': ref}, expected_status=409) + body={'credential': ref}, expected_status=http_client.CONFLICT) def test_get_ec2_dict_blob(self): """Ensure non-JSON blob data is correctly converted.""" @@ -215,7 +202,11 @@ class CredentialTestCase(CredentialBaseTestCase): r = self.get( '/credentials/%(credential_id)s' % { 'credential_id': credential_id}) - self.assertEqual(expected_blob, r.result['credential']['blob']) + + # use json.loads to transform the blobs back into Python dictionaries + # to avoid problems with the keys being in different orders. + self.assertEqual(json.loads(expected_blob), + json.loads(r.result['credential']['blob'])) def test_list_ec2_dict_blob(self): """Ensure non-JSON blob data is correctly converted.""" @@ -225,47 +216,49 @@ class CredentialTestCase(CredentialBaseTestCase): list_creds = list_r.result['credentials'] list_ids = [r['id'] for r in list_creds] self.assertIn(credential_id, list_ids) + # use json.loads to transform the blobs back into Python dictionaries + # to avoid problems with the keys being in different orders. for r in list_creds: if r['id'] == credential_id: - self.assertEqual(expected_blob, r['blob']) + self.assertEqual(json.loads(expected_blob), + json.loads(r['blob'])) def test_create_non_ec2_credential(self): - """Call ``POST /credentials`` for creating non-ec2 credential.""" - ref = self.new_credential_ref(user_id=self.user['id']) - blob = {"access": uuid.uuid4().hex, - "secret": uuid.uuid4().hex} - ref['blob'] = json.dumps(blob) - r = self.post( - '/credentials', - body={'credential': ref}) + """Test creating non-ec2 credential. + + Call ``POST /credentials``. + """ + blob, ref = unit.new_cert_credential(user_id=self.user['id']) + + r = self.post('/credentials', body={'credential': ref}) self.assertValidCredentialResponse(r, ref) # Assert credential id is not same as hash of access key id for # non-ec2 credentials - self.assertNotEqual(r.result['credential']['id'], - hashlib.sha256(blob['access']).hexdigest()) + access = blob['access'].encode('utf-8') + self.assertNotEqual(hashlib.sha256(access).hexdigest(), + r.result['credential']['id']) def test_create_ec2_credential_with_missing_project_id(self): - """Call ``POST /credentials`` for creating ec2 - credential with missing project_id. + """Test Creating ec2 credential with missing project_id. + + Call ``POST /credentials``. """ - ref = self.new_credential_ref(user_id=self.user['id']) - blob = {"access": uuid.uuid4().hex, - "secret": uuid.uuid4().hex} - ref['blob'] = json.dumps(blob) - ref['type'] = 'ec2' + _, ref = unit.new_ec2_credential(user_id=self.user['id'], + project_id=None) # Assert bad request status when missing project_id self.post( '/credentials', body={'credential': ref}, expected_status=http_client.BAD_REQUEST) def test_create_ec2_credential_with_invalid_blob(self): - """Call ``POST /credentials`` for creating ec2 - credential with invalid blob. + """Test creating ec2 credential with invalid blob. + + Call ``POST /credentials``. """ - ref = self.new_credential_ref(user_id=self.user['id'], - project_id=self.project_id) - ref['blob'] = '{"abc":"def"d}' - ref['type'] = 'ec2' + ref = unit.new_credential_ref(user_id=self.user['id'], + project_id=self.project_id, + blob='{"abc":"def"d}', + type=CRED_TYPE_EC2) # Assert bad request status when request contains invalid blob response = self.post( '/credentials', @@ -274,20 +267,21 @@ class CredentialTestCase(CredentialBaseTestCase): def test_create_credential_with_admin_token(self): # Make sure we can create credential with the static admin token - ref = self.new_credential_ref(user_id=self.user['id']) + ref = unit.new_credential_ref(user_id=self.user['id']) r = self.post( '/credentials', body={'credential': ref}, - token=CONF.admin_token) + token=self.get_admin_token()) self.assertValidCredentialResponse(r, ref) class TestCredentialTrustScoped(test_v3.RestfulTestCase): """Test credential with trust scoped token.""" + def setUp(self): super(TestCredentialTrustScoped, self).setUp() - self.trustee_user = self.new_user_ref(domain_id=self.domain_id) + self.trustee_user = unit.new_user_ref(domain_id=self.domain_id) password = self.trustee_user['password'] self.trustee_user = self.identity_api.create_user(self.trustee_user) self.trustee_user['password'] = password @@ -298,9 +292,12 @@ class TestCredentialTrustScoped(test_v3.RestfulTestCase): self.config_fixture.config(group='trust', enabled=True) def test_trust_scoped_ec2_credential(self): - """Call ``POST /credentials`` for creating ec2 credential.""" + """Test creating trust scoped ec2 credential. + + Call ``POST /credentials``. + """ # Create the trust - ref = self.new_trust_ref( + ref = unit.new_trust_ref( trustor_user_id=self.user_id, trustee_user_id=self.trustee_user_id, project_id=self.project_id, @@ -316,22 +313,15 @@ class TestCredentialTrustScoped(test_v3.RestfulTestCase): user_id=self.trustee_user['id'], password=self.trustee_user['password'], trust_id=trust['id']) - r = self.v3_authenticate_token(auth_data) - self.assertValidProjectTrustScopedTokenResponse(r, self.user) + r = self.v3_create_token(auth_data) + self.assertValidProjectScopedTokenResponse(r, self.user) trust_id = r.result['token']['OS-TRUST:trust']['id'] token_id = r.headers.get('X-Subject-Token') # Create the credential with the trust scoped token - ref = self.new_credential_ref(user_id=self.user['id'], - project_id=self.project_id) - blob = {"access": uuid.uuid4().hex, - "secret": uuid.uuid4().hex} - ref['blob'] = json.dumps(blob) - ref['type'] = 'ec2' - r = self.post( - '/credentials', - body={'credential': ref}, - token=token_id) + blob, ref = unit.new_ec2_credential(user_id=self.user['id'], + project_id=self.project_id) + r = self.post('/credentials', body={'credential': ref}, token=token_id) # We expect the response blob to contain the trust_id ret_ref = ref.copy() @@ -342,8 +332,9 @@ class TestCredentialTrustScoped(test_v3.RestfulTestCase): # Assert credential id is same as hash of access key id for # ec2 credentials - self.assertEqual(r.result['credential']['id'], - hashlib.sha256(blob['access']).hexdigest()) + access = blob['access'].encode('utf-8') + self.assertEqual(hashlib.sha256(access).hexdigest(), + r.result['credential']['id']) # Create second ec2 credential with the same access key id and check # for conflict. @@ -351,11 +342,12 @@ class TestCredentialTrustScoped(test_v3.RestfulTestCase): '/credentials', body={'credential': ref}, token=token_id, - expected_status=409) + expected_status=http_client.CONFLICT) class TestCredentialEc2(CredentialBaseTestCase): """Test v3 credential compatibility with ec2tokens.""" + def setUp(self): super(TestCredentialEc2, self).setUp() @@ -382,25 +374,19 @@ class TestCredentialEc2(CredentialBaseTestCase): r = self.post( '/ec2tokens', body={'ec2Credentials': sig_ref}, - expected_status=200) + expected_status=http_client.OK) self.assertValidTokenResponse(r) def test_ec2_credential_signature_validate(self): """Test signature validation with a v3 ec2 credential.""" - ref = self.new_credential_ref( - user_id=self.user['id'], - project_id=self.project_id) - blob = {"access": uuid.uuid4().hex, - "secret": uuid.uuid4().hex} - ref['blob'] = json.dumps(blob) - ref['type'] = 'ec2' - r = self.post( - '/credentials', - body={'credential': ref}) + blob, ref = unit.new_ec2_credential(user_id=self.user['id'], + project_id=self.project_id) + r = self.post('/credentials', body={'credential': ref}) self.assertValidCredentialResponse(r, ref) # Assert credential id is same as hash of access key id - self.assertEqual(r.result['credential']['id'], - hashlib.sha256(blob['access']).hexdigest()) + access = blob['access'].encode('utf-8') + self.assertEqual(hashlib.sha256(access).hexdigest(), + r.result['credential']['id']) cred_blob = json.loads(r.result['credential']['blob']) self.assertEqual(blob, cred_blob) @@ -409,7 +395,7 @@ class TestCredentialEc2(CredentialBaseTestCase): def test_ec2_credential_signature_validate_legacy(self): """Test signature validation with a legacy v3 ec2 credential.""" - cred_json, credential_id = self._create_dict_blob_credential() + cred_json, _ = self._create_dict_blob_credential() cred_blob = json.loads(cred_json) self._validate_signature(access=cred_blob['access'], secret=cred_blob['secret']) @@ -442,6 +428,19 @@ class TestCredentialEc2(CredentialBaseTestCase): self.assertThat(ec2_cred['links']['self'], matchers.EndsWith(uri)) + def test_ec2_cannot_get_non_ec2_credential(self): + access_key = uuid.uuid4().hex + cred_id = utils.hash_access_key(access_key) + non_ec2_cred = unit.new_credential_ref( + user_id=self.user_id, + project_id=self.project_id) + non_ec2_cred['id'] = cred_id + self.credential_api.create_credential(cred_id, non_ec2_cred) + uri = '/'.join([self._get_ec2_cred_uri(), access_key]) + # if access_key is not found, ec2 controller raises Unauthorized + # exception + self.get(uri, expected_status=http_client.UNAUTHORIZED) + def test_ec2_list_credentials(self): """Test ec2 credential listing.""" self._get_ec2_cred() @@ -452,13 +451,26 @@ class TestCredentialEc2(CredentialBaseTestCase): self.assertThat(r.result['links']['self'], matchers.EndsWith(uri)) + # non-EC2 credentials won't be fetched + non_ec2_cred = unit.new_credential_ref( + user_id=self.user_id, + project_id=self.project_id) + non_ec2_cred['type'] = uuid.uuid4().hex + self.credential_api.create_credential(non_ec2_cred['id'], + non_ec2_cred) + r = self.get(uri) + cred_list_2 = r.result['credentials'] + # still one element because non-EC2 credentials are not returned. + self.assertEqual(1, len(cred_list_2)) + self.assertEqual(cred_list[0], cred_list_2[0]) + def test_ec2_delete_credential(self): """Test ec2 credential deletion.""" ec2_cred = self._get_ec2_cred() uri = '/'.join([self._get_ec2_cred_uri(), ec2_cred['access']]) cred_from_credential_api = ( self.credential_api - .list_credentials_for_user(self.user_id)) + .list_credentials_for_user(self.user_id, type=CRED_TYPE_EC2)) self.assertEqual(1, len(cred_from_credential_api)) self.delete(uri) self.assertRaises(exception.CredentialNotFound, diff --git a/keystone-moon/keystone/tests/unit/test_v3_domain_config.py b/keystone-moon/keystone/tests/unit/test_v3_domain_config.py index 701cd3cf..ee716081 100644 --- a/keystone-moon/keystone/tests/unit/test_v3_domain_config.py +++ b/keystone-moon/keystone/tests/unit/test_v3_domain_config.py @@ -17,6 +17,7 @@ from oslo_config import cfg from six.moves import http_client from keystone import exception +from keystone.tests import unit from keystone.tests.unit import test_v3 @@ -29,7 +30,7 @@ class DomainConfigTestCase(test_v3.RestfulTestCase): def setUp(self): super(DomainConfigTestCase, self).setUp() - self.domain = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + self.domain = unit.new_domain_ref() self.resource_api.create_domain(self.domain['id'], self.domain) self.config = {'ldap': {'url': uuid.uuid4().hex, 'user_tree_dn': uuid.uuid4().hex}, @@ -40,21 +41,34 @@ class DomainConfigTestCase(test_v3.RestfulTestCase): url = '/domains/%(domain_id)s/config' % { 'domain_id': self.domain['id']} r = self.put(url, body={'config': self.config}, - expected_status=201) + expected_status=http_client.CREATED) res = self.domain_config_api.get_config(self.domain['id']) self.assertEqual(self.config, r.result['config']) self.assertEqual(self.config, res) + def test_create_config_invalid_domain(self): + """Call ``PUT /domains/{domain_id}/config`` + + While creating Identity API-based domain config with an invalid domain + id provided, the request shall be rejected with a response, 404 domain + not found. + """ + invalid_domain_id = uuid.uuid4().hex + url = '/domains/%(domain_id)s/config' % { + 'domain_id': invalid_domain_id} + self.put(url, body={'config': self.config}, + expected_status=exception.DomainNotFound.code) + def test_create_config_twice(self): """Check multiple creates don't throw error""" self.put('/domains/%(domain_id)s/config' % { 'domain_id': self.domain['id']}, body={'config': self.config}, - expected_status=201) + expected_status=http_client.CREATED) self.put('/domains/%(domain_id)s/config' % { 'domain_id': self.domain['id']}, body={'config': self.config}, - expected_status=200) + expected_status=http_client.OK) def test_delete_config(self): """Call ``DELETE /domains{domain_id}/config``.""" @@ -65,6 +79,19 @@ class DomainConfigTestCase(test_v3.RestfulTestCase): 'domain_id': self.domain['id']}, expected_status=exception.DomainConfigNotFound.code) + def test_delete_config_invalid_domain(self): + """Call ``DELETE /domains{domain_id}/config`` + + While deleting Identity API-based domain config with an invalid domain + id provided, the request shall be rejected with a response, 404 domain + not found. + """ + self.domain_config_api.create_config(self.domain['id'], self.config) + invalid_domain_id = uuid.uuid4().hex + self.delete('/domains/%(domain_id)s/config' % { + 'domain_id': invalid_domain_id}, + expected_status=exception.DomainNotFound.code) + def test_delete_config_by_group(self): """Call ``DELETE /domains{domain_id}/config/{group}``.""" self.domain_config_api.create_config(self.domain['id'], self.config) @@ -73,6 +100,19 @@ class DomainConfigTestCase(test_v3.RestfulTestCase): res = self.domain_config_api.get_config(self.domain['id']) self.assertNotIn('ldap', res) + def test_delete_config_by_group_invalid_domain(self): + """Call ``DELETE /domains{domain_id}/config/{group}`` + + While deleting Identity API-based domain config by group with an + invalid domain id provided, the request shall be rejected with a + response 404 domain not found. + """ + self.domain_config_api.create_config(self.domain['id'], self.config) + invalid_domain_id = uuid.uuid4().hex + self.delete('/domains/%(domain_id)s/config/ldap' % { + 'domain_id': invalid_domain_id}, + expected_status=exception.DomainNotFound.code) + def test_get_head_config(self): """Call ``GET & HEAD for /domains{domain_id}/config``.""" self.domain_config_api.create_config(self.domain['id'], self.config) @@ -80,7 +120,7 @@ class DomainConfigTestCase(test_v3.RestfulTestCase): 'domain_id': self.domain['id']} r = self.get(url) self.assertEqual(self.config, r.result['config']) - self.head(url, expected_status=200) + self.head(url, expected_status=http_client.OK) def test_get_config_by_group(self): """Call ``GET & HEAD /domains{domain_id}/config/{group}``.""" @@ -89,7 +129,20 @@ class DomainConfigTestCase(test_v3.RestfulTestCase): 'domain_id': self.domain['id']} r = self.get(url) self.assertEqual({'ldap': self.config['ldap']}, r.result['config']) - self.head(url, expected_status=200) + self.head(url, expected_status=http_client.OK) + + def test_get_config_by_group_invalid_domain(self): + """Call ``GET & HEAD /domains{domain_id}/config/{group}`` + + While retrieving Identity API-based domain config by group with an + invalid domain id provided, the request shall be rejected with a + response 404 domain not found. + """ + self.domain_config_api.create_config(self.domain['id'], self.config) + invalid_domain_id = uuid.uuid4().hex + self.get('/domains/%(domain_id)s/config/ldap' % { + 'domain_id': invalid_domain_id}, + expected_status=exception.DomainNotFound.code) def test_get_config_by_option(self): """Call ``GET & HEAD /domains{domain_id}/config/{group}/{option}``.""" @@ -99,7 +152,20 @@ class DomainConfigTestCase(test_v3.RestfulTestCase): r = self.get(url) self.assertEqual({'url': self.config['ldap']['url']}, r.result['config']) - self.head(url, expected_status=200) + self.head(url, expected_status=http_client.OK) + + def test_get_config_by_option_invalid_domain(self): + """Call ``GET & HEAD /domains{domain_id}/config/{group}/{option}`` + + While retrieving Identity API-based domain config by option with an + invalid domain id provided, the request shall be rejected with a + response 404 domain not found. + """ + self.domain_config_api.create_config(self.domain['id'], self.config) + invalid_domain_id = uuid.uuid4().hex + self.get('/domains/%(domain_id)s/config/ldap/url' % { + 'domain_id': invalid_domain_id}, + expected_status=exception.DomainNotFound.code) def test_get_non_existant_config(self): """Call ``GET /domains{domain_id}/config when no config defined``.""" @@ -107,6 +173,18 @@ class DomainConfigTestCase(test_v3.RestfulTestCase): 'domain_id': self.domain['id']}, expected_status=http_client.NOT_FOUND) + def test_get_non_existant_config_invalid_domain(self): + """Call ``GET /domains{domain_id}/config when no config defined`` + + While retrieving non-existent Identity API-based domain config with an + invalid domain id provided, the request shall be rejected with a + response 404 domain not found. + """ + invalid_domain_id = uuid.uuid4().hex + self.get('/domains/%(domain_id)s/config' % { + 'domain_id': invalid_domain_id}, + expected_status=exception.DomainNotFound.code) + def test_get_non_existant_config_group(self): """Call ``GET /domains{domain_id}/config/{group_not_exist}``.""" config = {'ldap': {'url': uuid.uuid4().hex}} @@ -115,6 +193,20 @@ class DomainConfigTestCase(test_v3.RestfulTestCase): 'domain_id': self.domain['id']}, expected_status=http_client.NOT_FOUND) + def test_get_non_existant_config_group_invalid_domain(self): + """Call ``GET /domains{domain_id}/config/{group_not_exist}`` + + While retrieving non-existent Identity API-based domain config group + with an invalid domain id provided, the request shall be rejected with + a response, 404 domain not found. + """ + config = {'ldap': {'url': uuid.uuid4().hex}} + self.domain_config_api.create_config(self.domain['id'], config) + invalid_domain_id = uuid.uuid4().hex + self.get('/domains/%(domain_id)s/config/identity' % { + 'domain_id': invalid_domain_id}, + expected_status=exception.DomainNotFound.code) + def test_get_non_existant_config_option(self): """Call ``GET /domains{domain_id}/config/group/{option_not_exist}``.""" config = {'ldap': {'url': uuid.uuid4().hex}} @@ -123,6 +215,20 @@ class DomainConfigTestCase(test_v3.RestfulTestCase): 'domain_id': self.domain['id']}, expected_status=http_client.NOT_FOUND) + def test_get_non_existant_config_option_invalid_domain(self): + """Call ``GET /domains{domain_id}/config/group/{option_not_exist}`` + + While retrieving non-existent Identity API-based domain config option + with an invalid domain id provided, the request shall be rejected with + a response, 404 domain not found. + """ + config = {'ldap': {'url': uuid.uuid4().hex}} + self.domain_config_api.create_config(self.domain['id'], config) + invalid_domain_id = uuid.uuid4().hex + self.get('/domains/%(domain_id)s/config/ldap/user_tree_dn' % { + 'domain_id': invalid_domain_id}, + expected_status=exception.DomainNotFound.code) + def test_update_config(self): """Call ``PATCH /domains/{domain_id}/config``.""" self.domain_config_api.create_config(self.domain['id'], self.config) @@ -139,6 +245,22 @@ class DomainConfigTestCase(test_v3.RestfulTestCase): self.assertEqual(expected_config, r.result['config']) self.assertEqual(expected_config, res) + def test_update_config_invalid_domain(self): + """Call ``PATCH /domains/{domain_id}/config`` + + While updating Identity API-based domain config with an invalid domain + id provided, the request shall be rejected with a response, 404 domain + not found. + """ + self.domain_config_api.create_config(self.domain['id'], self.config) + new_config = {'ldap': {'url': uuid.uuid4().hex}, + 'identity': {'driver': uuid.uuid4().hex}} + invalid_domain_id = uuid.uuid4().hex + self.patch('/domains/%(domain_id)s/config' % { + 'domain_id': invalid_domain_id}, + body={'config': new_config}, + expected_status=exception.DomainNotFound.code) + def test_update_config_group(self): """Call ``PATCH /domains/{domain_id}/config/{group}``.""" self.domain_config_api.create_config(self.domain['id'], self.config) @@ -155,6 +277,22 @@ class DomainConfigTestCase(test_v3.RestfulTestCase): self.assertEqual(expected_config, r.result['config']) self.assertEqual(expected_config, res) + def test_update_config_group_invalid_domain(self): + """Call ``PATCH /domains/{domain_id}/config/{group}`` + + While updating Identity API-based domain config group with an invalid + domain id provided, the request shall be rejected with a response, + 404 domain not found. + """ + self.domain_config_api.create_config(self.domain['id'], self.config) + new_config = {'ldap': {'url': uuid.uuid4().hex, + 'user_filter': uuid.uuid4().hex}} + invalid_domain_id = uuid.uuid4().hex + self.patch('/domains/%(domain_id)s/config/ldap' % { + 'domain_id': invalid_domain_id}, + body={'config': new_config}, + expected_status=exception.DomainNotFound.code) + def test_update_config_invalid_group(self): """Call ``PATCH /domains/{domain_id}/config/{invalid_group}``.""" self.domain_config_api.create_config(self.domain['id'], self.config) @@ -178,6 +316,24 @@ class DomainConfigTestCase(test_v3.RestfulTestCase): body={'config': new_config}, expected_status=http_client.NOT_FOUND) + def test_update_config_invalid_group_invalid_domain(self): + """Call ``PATCH /domains/{domain_id}/config/{invalid_group}`` + + While updating Identity API-based domain config with an invalid group + and an invalid domain id provided, the request shall be rejected + with a response, 404 domain not found. + """ + self.domain_config_api.create_config(self.domain['id'], self.config) + invalid_group = uuid.uuid4().hex + new_config = {invalid_group: {'url': uuid.uuid4().hex, + 'user_filter': uuid.uuid4().hex}} + invalid_domain_id = uuid.uuid4().hex + self.patch('/domains/%(domain_id)s/config/%(invalid_group)s' % { + 'domain_id': invalid_domain_id, + 'invalid_group': invalid_group}, + body={'config': new_config}, + expected_status=exception.DomainNotFound.code) + def test_update_config_option(self): """Call ``PATCH /domains/{domain_id}/config/{group}/{option}``.""" self.domain_config_api.create_config(self.domain['id'], self.config) @@ -191,6 +347,21 @@ class DomainConfigTestCase(test_v3.RestfulTestCase): self.assertEqual(expected_config, r.result['config']) self.assertEqual(expected_config, res) + def test_update_config_option_invalid_domain(self): + """Call ``PATCH /domains/{domain_id}/config/{group}/{option}`` + + While updating Identity API-based domain config option with an invalid + domain id provided, the request shall be rejected with a response, 404 + domain not found. + """ + self.domain_config_api.create_config(self.domain['id'], self.config) + new_config = {'url': uuid.uuid4().hex} + invalid_domain_id = uuid.uuid4().hex + self.patch('/domains/%(domain_id)s/config/ldap/url' % { + 'domain_id': invalid_domain_id}, + body={'config': new_config}, + expected_status=exception.DomainNotFound.code) + def test_update_config_invalid_option(self): """Call ``PATCH /domains/{domain_id}/config/{group}/{invalid}``.""" self.domain_config_api.create_config(self.domain['id'], self.config) @@ -212,3 +383,77 @@ class DomainConfigTestCase(test_v3.RestfulTestCase): 'domain_id': self.domain['id']}, body={'config': new_config}, expected_status=http_client.NOT_FOUND) + + def test_update_config_invalid_option_invalid_domain(self): + """Call ``PATCH /domains/{domain_id}/config/{group}/{invalid}`` + + While updating Identity API-based domain config with an invalid option + and an invalid domain id provided, the request shall be rejected + with a response, 404 domain not found. + """ + self.domain_config_api.create_config(self.domain['id'], self.config) + invalid_option = uuid.uuid4().hex + new_config = {'ldap': {invalid_option: uuid.uuid4().hex}} + invalid_domain_id = uuid.uuid4().hex + self.patch( + '/domains/%(domain_id)s/config/ldap/%(invalid_option)s' % { + 'domain_id': invalid_domain_id, + 'invalid_option': invalid_option}, + body={'config': new_config}, + expected_status=exception.DomainNotFound.code) + + def test_get_config_default(self): + """Call ``GET /domains/config/default``.""" + # Create a config that overrides a few of the options so that we can + # check that only the defaults are returned. + self.domain_config_api.create_config(self.domain['id'], self.config) + url = '/domains/config/default' + r = self.get(url) + default_config = r.result['config'] + for group in default_config: + for option in default_config[group]: + self.assertEqual(getattr(getattr(CONF, group), option), + default_config[group][option]) + + def test_get_config_default_by_group(self): + """Call ``GET /domains/config/{group}/default``.""" + # Create a config that overrides a few of the options so that we can + # check that only the defaults are returned. + self.domain_config_api.create_config(self.domain['id'], self.config) + url = '/domains/config/ldap/default' + r = self.get(url) + default_config = r.result['config'] + for option in default_config['ldap']: + self.assertEqual(getattr(CONF.ldap, option), + default_config['ldap'][option]) + + def test_get_config_default_by_option(self): + """Call ``GET /domains/config/{group}/{option}/default``.""" + # Create a config that overrides a few of the options so that we can + # check that only the defaults are returned. + self.domain_config_api.create_config(self.domain['id'], self.config) + url = '/domains/config/ldap/url/default' + r = self.get(url) + default_config = r.result['config'] + self.assertEqual(CONF.ldap.url, default_config['url']) + + def test_get_config_default_by_invalid_group(self): + """Call ``GET for /domains/config/{bad-group}/default``.""" + # First try a valid group, but one we don't support for domain config + self.get('/domains/config/resouce/default', + expected_status=http_client.FORBIDDEN) + + # Now try a totally invalid group + url = '/domains/config/%s/default' % uuid.uuid4().hex + self.get(url, expected_status=http_client.FORBIDDEN) + + def test_get_config_default_by_invalid_option(self): + """Call ``GET for /domains/config/{group}/{bad-option}/default``.""" + # First try a valid option, but one we don't support for domain config, + # i.e. one that is in the sensitive options list + self.get('/domains/config/ldap/password/default', + expected_status=http_client.FORBIDDEN) + + # Now try a totally invalid option + url = '/domains/config/ldap/%s/default' % uuid.uuid4().hex + self.get(url, expected_status=http_client.FORBIDDEN) diff --git a/keystone-moon/keystone/tests/unit/test_v3_endpoint_policy.py b/keystone-moon/keystone/tests/unit/test_v3_endpoint_policy.py index 3423d2d8..9fee8d2b 100644 --- a/keystone-moon/keystone/tests/unit/test_v3_endpoint_policy.py +++ b/keystone-moon/keystone/tests/unit/test_v3_endpoint_policy.py @@ -15,6 +15,7 @@ from six.moves import http_client from testtools import matchers +from keystone.tests import unit from keystone.tests.unit import test_v3 @@ -31,13 +32,15 @@ class EndpointPolicyTestCase(test_v3.RestfulTestCase): def setUp(self): super(EndpointPolicyTestCase, self).setUp() - self.policy = self.new_policy_ref() + self.policy = unit.new_policy_ref() self.policy_api.create_policy(self.policy['id'], self.policy) - self.service = self.new_service_ref() + self.service = unit.new_service_ref() self.catalog_api.create_service(self.service['id'], self.service) - self.endpoint = self.new_endpoint_ref(self.service['id'], enabled=True) + self.endpoint = unit.new_endpoint_ref(self.service['id'], enabled=True, + interface='public', + region_id=self.region_id) self.catalog_api.create_endpoint(self.endpoint['id'], self.endpoint) - self.region = self.new_region_ref() + self.region = unit.new_region_ref() self.catalog_api.create_region(self.region) def assert_head_and_get_return_same_response(self, url, expected_status): @@ -53,12 +56,14 @@ class EndpointPolicyTestCase(test_v3.RestfulTestCase): url, expected_status=http_client.NOT_FOUND) - self.put(url, expected_status=204) + self.put(url) # test that the new resource is accessible. - self.assert_head_and_get_return_same_response(url, expected_status=204) + self.assert_head_and_get_return_same_response( + url, + expected_status=http_client.NO_CONTENT) - self.delete(url, expected_status=204) + self.delete(url) # test that the deleted resource is no longer accessible self.assert_head_and_get_return_same_response( @@ -67,7 +72,6 @@ class EndpointPolicyTestCase(test_v3.RestfulTestCase): def test_crud_for_policy_for_explicit_endpoint(self): """PUT, HEAD and DELETE for explicit endpoint policy.""" - url = ('/policies/%(policy_id)s/OS-ENDPOINT-POLICY' '/endpoints/%(endpoint_id)s') % { 'policy_id': self.policy['id'], @@ -76,7 +80,6 @@ class EndpointPolicyTestCase(test_v3.RestfulTestCase): def test_crud_for_policy_for_service(self): """PUT, HEAD and DELETE for service endpoint policy.""" - url = ('/policies/%(policy_id)s/OS-ENDPOINT-POLICY' '/services/%(service_id)s') % { 'policy_id': self.policy['id'], @@ -85,7 +88,6 @@ class EndpointPolicyTestCase(test_v3.RestfulTestCase): def test_crud_for_policy_for_region_and_service(self): """PUT, HEAD and DELETE for region and service endpoint policy.""" - url = ('/policies/%(policy_id)s/OS-ENDPOINT-POLICY' '/services/%(service_id)s/regions/%(region_id)s') % { 'policy_id': self.policy['id'], @@ -95,37 +97,31 @@ class EndpointPolicyTestCase(test_v3.RestfulTestCase): def test_get_policy_for_endpoint(self): """GET /endpoints/{endpoint_id}/policy.""" - self.put('/policies/%(policy_id)s/OS-ENDPOINT-POLICY' '/endpoints/%(endpoint_id)s' % { 'policy_id': self.policy['id'], - 'endpoint_id': self.endpoint['id']}, - expected_status=204) + 'endpoint_id': self.endpoint['id']}) self.head('/endpoints/%(endpoint_id)s/OS-ENDPOINT-POLICY' '/policy' % { 'endpoint_id': self.endpoint['id']}, - expected_status=200) + expected_status=http_client.OK) r = self.get('/endpoints/%(endpoint_id)s/OS-ENDPOINT-POLICY' '/policy' % { - 'endpoint_id': self.endpoint['id']}, - expected_status=200) + 'endpoint_id': self.endpoint['id']}) self.assertValidPolicyResponse(r, ref=self.policy) def test_list_endpoints_for_policy(self): """GET /policies/%(policy_id}/endpoints.""" - self.put('/policies/%(policy_id)s/OS-ENDPOINT-POLICY' '/endpoints/%(endpoint_id)s' % { 'policy_id': self.policy['id'], - 'endpoint_id': self.endpoint['id']}, - expected_status=204) + 'endpoint_id': self.endpoint['id']}) r = self.get('/policies/%(policy_id)s/OS-ENDPOINT-POLICY' '/endpoints' % { - 'policy_id': self.policy['id']}, - expected_status=200) + 'policy_id': self.policy['id']}) self.assertValidEndpointListResponse(r, ref=self.endpoint) self.assertThat(r.result.get('endpoints'), matchers.HasLength(1)) @@ -135,8 +131,8 @@ class EndpointPolicyTestCase(test_v3.RestfulTestCase): 'policy_id': self.policy['id'], 'endpoint_id': self.endpoint['id']} - self.put(url, expected_status=204) - self.head(url, expected_status=204) + self.put(url) + self.head(url) self.delete('/endpoints/%(endpoint_id)s' % { 'endpoint_id': self.endpoint['id']}) @@ -150,8 +146,8 @@ class EndpointPolicyTestCase(test_v3.RestfulTestCase): 'service_id': self.service['id'], 'region_id': self.region['id']} - self.put(url, expected_status=204) - self.head(url, expected_status=204) + self.put(url) + self.head(url) self.delete('/regions/%(region_id)s' % { 'region_id': self.region['id']}) @@ -165,8 +161,8 @@ class EndpointPolicyTestCase(test_v3.RestfulTestCase): 'service_id': self.service['id'], 'region_id': self.region['id']} - self.put(url, expected_status=204) - self.head(url, expected_status=204) + self.put(url) + self.head(url) self.delete('/services/%(service_id)s' % { 'service_id': self.service['id']}) @@ -179,8 +175,8 @@ class EndpointPolicyTestCase(test_v3.RestfulTestCase): 'policy_id': self.policy['id'], 'service_id': self.service['id']} - self.put(url, expected_status=204) - self.get(url, expected_status=204) + self.put(url) + self.get(url, expected_status=http_client.NO_CONTENT) self.delete('/policies/%(policy_id)s' % { 'policy_id': self.policy['id']}) @@ -193,8 +189,8 @@ class EndpointPolicyTestCase(test_v3.RestfulTestCase): 'policy_id': self.policy['id'], 'service_id': self.service['id']} - self.put(url, expected_status=204) - self.get(url, expected_status=204) + self.put(url) + self.get(url, expected_status=http_client.NO_CONTENT) self.delete('/services/%(service_id)s' % { 'service_id': self.service['id']}) diff --git a/keystone-moon/keystone/tests/unit/test_v3_federation.py b/keystone-moon/keystone/tests/unit/test_v3_federation.py index 4d7dcaab..f4ec8e51 100644 --- a/keystone-moon/keystone/tests/unit/test_v3_federation.py +++ b/keystone-moon/keystone/tests/unit/test_v3_federation.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import copy import os import random from testtools import matchers @@ -19,7 +20,8 @@ import fixtures from lxml import etree import mock from oslo_config import cfg -from oslo_log import log +from oslo_log import versionutils +from oslo_serialization import jsonutils from oslo_utils import importutils from oslotest import mockpatch import saml2 @@ -33,22 +35,24 @@ if not xmldsig: from keystone.auth import controllers as auth_controllers from keystone.common import environment -from keystone.contrib.federation import controllers as federation_controllers -from keystone.contrib.federation import idp as keystone_idp +from keystone.contrib.federation import routers from keystone import exception +from keystone.federation import controllers as federation_controllers +from keystone.federation import idp as keystone_idp from keystone import notifications +from keystone.tests import unit from keystone.tests.unit import core from keystone.tests.unit import federation_fixtures from keystone.tests.unit import ksfixtures from keystone.tests.unit import mapping_fixtures from keystone.tests.unit import test_v3 +from keystone.tests.unit import utils from keystone.token.providers import common as token_common subprocess = environment.subprocess CONF = cfg.CONF -LOG = log.getLogger(__name__) ROOTDIR = os.path.dirname(os.path.abspath(__file__)) XMLDIR = os.path.join(ROOTDIR, 'saml2/') @@ -59,8 +63,12 @@ def dummy_validator(*args, **kwargs): class FederationTests(test_v3.RestfulTestCase): - EXTENSION_NAME = 'federation' - EXTENSION_TO_ADD = 'federation_extension' + @mock.patch.object(versionutils, 'report_deprecated_feature') + def test_exception_happens(self, mock_deprecator): + routers.FederationExtension(mock.ANY) + mock_deprecator.assert_called_once_with(mock.ANY, mock.ANY) + args, _kwargs = mock_deprecator.call_args + self.assertIn("Remove federation_extension from", args[1]) class FederatedSetupMixin(object): @@ -137,7 +145,6 @@ class FederatedSetupMixin(object): def assertValidMappedUser(self, token): """Check if user object meets all the criteria.""" - user = token['user'] self.assertIn('id', user) self.assertIn('name', user) @@ -209,66 +216,62 @@ class FederatedSetupMixin(object): def load_federation_sample_data(self): """Inject additional data.""" - # Create and add domains - self.domainA = self.new_domain_ref() + self.domainA = unit.new_domain_ref() self.resource_api.create_domain(self.domainA['id'], self.domainA) - self.domainB = self.new_domain_ref() + self.domainB = unit.new_domain_ref() self.resource_api.create_domain(self.domainB['id'], self.domainB) - self.domainC = self.new_domain_ref() + self.domainC = unit.new_domain_ref() self.resource_api.create_domain(self.domainC['id'], self.domainC) - self.domainD = self.new_domain_ref() + self.domainD = unit.new_domain_ref() self.resource_api.create_domain(self.domainD['id'], self.domainD) # Create and add projects - self.proj_employees = self.new_project_ref( + self.proj_employees = unit.new_project_ref( domain_id=self.domainA['id']) self.resource_api.create_project(self.proj_employees['id'], self.proj_employees) - self.proj_customers = self.new_project_ref( + self.proj_customers = unit.new_project_ref( domain_id=self.domainA['id']) self.resource_api.create_project(self.proj_customers['id'], self.proj_customers) - self.project_all = self.new_project_ref( + self.project_all = unit.new_project_ref( domain_id=self.domainA['id']) self.resource_api.create_project(self.project_all['id'], self.project_all) - self.project_inherited = self.new_project_ref( + self.project_inherited = unit.new_project_ref( domain_id=self.domainD['id']) self.resource_api.create_project(self.project_inherited['id'], self.project_inherited) # Create and add groups - self.group_employees = self.new_group_ref( - domain_id=self.domainA['id']) + self.group_employees = unit.new_group_ref(domain_id=self.domainA['id']) self.group_employees = ( self.identity_api.create_group(self.group_employees)) - self.group_customers = self.new_group_ref( - domain_id=self.domainA['id']) + self.group_customers = unit.new_group_ref(domain_id=self.domainA['id']) self.group_customers = ( self.identity_api.create_group(self.group_customers)) - self.group_admins = self.new_group_ref( - domain_id=self.domainA['id']) + self.group_admins = unit.new_group_ref(domain_id=self.domainA['id']) self.group_admins = self.identity_api.create_group(self.group_admins) # Create and add roles - self.role_employee = self.new_role_ref() + self.role_employee = unit.new_role_ref() self.role_api.create_role(self.role_employee['id'], self.role_employee) - self.role_customer = self.new_role_ref() + self.role_customer = unit.new_role_ref() self.role_api.create_role(self.role_customer['id'], self.role_customer) - self.role_admin = self.new_role_ref() + self.role_admin = unit.new_role_ref() self.role_api.create_role(self.role_admin['id'], self.role_admin) # Employees can access @@ -774,7 +777,7 @@ class FederatedSetupMixin(object): self.domainC['id']) -class FederatedIdentityProviderTests(FederationTests): +class FederatedIdentityProviderTests(test_v3.RestfulTestCase): """A test class for Identity Providers.""" idp_keys = ['description', 'enabled'] @@ -815,7 +818,7 @@ class FederatedIdentityProviderTests(FederationTests): if body is None: body = self._http_idp_input() resp = self.put(url, body={'identity_provider': body}, - expected_status=201) + expected_status=http_client.CREATED) return resp def _http_idp_input(self, **kwargs): @@ -856,7 +859,6 @@ class FederatedIdentityProviderTests(FederationTests): def test_create_idp(self): """Creates the IdentityProvider entity associated to remote_ids.""" - keys_to_check = list(self.idp_keys) body = self.default_body.copy() body['description'] = uuid.uuid4().hex @@ -867,7 +869,6 @@ class FederatedIdentityProviderTests(FederationTests): def test_create_idp_remote(self): """Creates the IdentityProvider entity associated to remote_ids.""" - keys_to_check = list(self.idp_keys) keys_to_check.append('remote_ids') body = self.default_body.copy() @@ -886,10 +887,9 @@ class FederatedIdentityProviderTests(FederationTests): A remote_id is the same for both so the second IdP is not created because of the uniqueness of the remote_ids - Expect HTTP 409 code for the latter call. + Expect HTTP 409 Conflict code for the latter call. """ - body = self.default_body.copy() repeated_remote_id = uuid.uuid4().hex body['remote_ids'] = [uuid.uuid4().hex, @@ -901,12 +901,15 @@ class FederatedIdentityProviderTests(FederationTests): url = self.base_url(suffix=uuid.uuid4().hex) body['remote_ids'] = [uuid.uuid4().hex, repeated_remote_id] - self.put(url, body={'identity_provider': body}, - expected_status=http_client.CONFLICT) + resp = self.put(url, body={'identity_provider': body}, + expected_status=http_client.CONFLICT) + + resp_data = jsonutils.loads(resp.body) + self.assertIn('Duplicate remote ID', + resp_data.get('error', {}).get('message')) def test_create_idp_remote_empty(self): """Creates an IdP with empty remote_ids.""" - keys_to_check = list(self.idp_keys) keys_to_check.append('remote_ids') body = self.default_body.copy() @@ -919,7 +922,6 @@ class FederatedIdentityProviderTests(FederationTests): def test_create_idp_remote_none(self): """Creates an IdP with a None remote_ids.""" - keys_to_check = list(self.idp_keys) keys_to_check.append('remote_ids') body = self.default_body.copy() @@ -986,6 +988,37 @@ class FederatedIdentityProviderTests(FederationTests): self.assertEqual(sorted(body['remote_ids']), sorted(returned_idp.get('remote_ids'))) + def test_update_idp_remote_repeated(self): + """Update an IdentityProvider entity reusing a remote_id. + + A remote_id is the same for both so the second IdP is not + updated because of the uniqueness of the remote_ids. + + Expect HTTP 409 Conflict code for the latter call. + + """ + # Create first identity provider + body = self.default_body.copy() + repeated_remote_id = uuid.uuid4().hex + body['remote_ids'] = [uuid.uuid4().hex, + repeated_remote_id] + self._create_default_idp(body=body) + + # Create second identity provider (without remote_ids) + body = self.default_body.copy() + default_resp = self._create_default_idp(body=body) + default_idp = self._fetch_attribute_from_response(default_resp, + 'identity_provider') + idp_id = default_idp.get('id') + url = self.base_url(suffix=idp_id) + + body['remote_ids'] = [repeated_remote_id] + resp = self.patch(url, body={'identity_provider': body}, + expected_status=http_client.CONFLICT) + resp_data = jsonutils.loads(resp.body) + self.assertIn('Duplicate remote ID', + resp_data['error']['message']) + def test_list_idps(self, iterations=5): """Lists all available IdentityProviders. @@ -1018,18 +1051,73 @@ class FederatedIdentityProviderTests(FederationTests): ids_intersection = entities_ids.intersection(ids) self.assertEqual(ids_intersection, ids) + def test_filter_list_idp_by_id(self): + def get_id(resp): + r = self._fetch_attribute_from_response(resp, + 'identity_provider') + return r.get('id') + + idp1_id = get_id(self._create_default_idp()) + idp2_id = get_id(self._create_default_idp()) + + # list the IdP, should get two IdP. + url = self.base_url() + resp = self.get(url) + entities = self._fetch_attribute_from_response(resp, + 'identity_providers') + entities_ids = [e['id'] for e in entities] + self.assertItemsEqual(entities_ids, [idp1_id, idp2_id]) + + # filter the IdP by ID. + url = self.base_url() + '?id=' + idp1_id + resp = self.get(url) + filtered_service_list = resp.json['identity_providers'] + self.assertThat(filtered_service_list, matchers.HasLength(1)) + self.assertEqual(idp1_id, filtered_service_list[0].get('id')) + + def test_filter_list_idp_by_enabled(self): + def get_id(resp): + r = self._fetch_attribute_from_response(resp, + 'identity_provider') + return r.get('id') + + idp1_id = get_id(self._create_default_idp()) + + body = self.default_body.copy() + body['enabled'] = False + idp2_id = get_id(self._create_default_idp(body=body)) + + # list the IdP, should get two IdP. + url = self.base_url() + resp = self.get(url) + entities = self._fetch_attribute_from_response(resp, + 'identity_providers') + entities_ids = [e['id'] for e in entities] + self.assertItemsEqual(entities_ids, [idp1_id, idp2_id]) + + # filter the IdP by 'enabled'. + url = self.base_url() + '?enabled=True' + resp = self.get(url) + filtered_service_list = resp.json['identity_providers'] + self.assertThat(filtered_service_list, matchers.HasLength(1)) + self.assertEqual(idp1_id, filtered_service_list[0].get('id')) + def test_check_idp_uniqueness(self): """Add same IdP twice. - Expect HTTP 409 code for the latter call. + Expect HTTP 409 Conflict code for the latter call. """ url = self.base_url(suffix=uuid.uuid4().hex) body = self._http_idp_input() self.put(url, body={'identity_provider': body}, - expected_status=201) - self.put(url, body={'identity_provider': body}, - expected_status=http_client.CONFLICT) + expected_status=http_client.CREATED) + resp = self.put(url, body={'identity_provider': body}, + expected_status=http_client.CONFLICT) + + resp_data = jsonutils.loads(resp.body) + self.assertIn('Duplicate entry', + resp_data.get('error', {}).get('message')) def test_get_idp(self): """Create and later fetch IdP.""" @@ -1047,7 +1135,7 @@ class FederatedIdentityProviderTests(FederationTests): def test_get_nonexisting_idp(self): """Fetch nonexisting IdP entity. - Expected HTTP 404 status code. + Expected HTTP 404 Not Found status code. """ idp_id = uuid.uuid4().hex @@ -1059,7 +1147,7 @@ class FederatedIdentityProviderTests(FederationTests): def test_delete_existing_idp(self): """Create and later delete IdP. - Expect HTTP 404 for the GET IdP call. + Expect HTTP 404 Not Found for the GET IdP call. """ default_resp = self._create_default_idp() default_idp = self._fetch_attribute_from_response(default_resp, @@ -1072,7 +1160,6 @@ class FederatedIdentityProviderTests(FederationTests): def test_delete_idp_also_deletes_assigned_protocols(self): """Deleting an IdP will delete its assigned protocol.""" - # create default IdP default_resp = self._create_default_idp() default_idp = self._fetch_attribute_from_response(default_resp, @@ -1084,7 +1171,7 @@ class FederatedIdentityProviderTests(FederationTests): idp_url = self.base_url(suffix=idp_id) # assign protocol to IdP - kwargs = {'expected_status': 201} + kwargs = {'expected_status': http_client.CREATED} resp, idp_id, proto = self._assign_protocol_to_idp( url=url, idp_id=idp_id, @@ -1100,7 +1187,7 @@ class FederatedIdentityProviderTests(FederationTests): def test_delete_nonexisting_idp(self): """Delete nonexisting IdP. - Expect HTTP 404 for the GET IdP call. + Expect HTTP 404 Not Found for the GET IdP call. """ idp_id = uuid.uuid4().hex url = self.base_url(suffix=idp_id) @@ -1145,7 +1232,7 @@ class FederatedIdentityProviderTests(FederationTests): def test_update_idp_immutable_attributes(self): """Update IdP's immutable parameters. - Expect HTTP FORBIDDEN. + Expect HTTP BAD REQUEST. """ default_resp = self._create_default_idp() @@ -1160,12 +1247,12 @@ class FederatedIdentityProviderTests(FederationTests): url = self.base_url(suffix=idp_id) self.patch(url, body={'identity_provider': body}, - expected_status=http_client.FORBIDDEN) + expected_status=http_client.BAD_REQUEST) def test_update_nonexistent_idp(self): """Update nonexistent IdP - Expect HTTP 404 code. + Expect HTTP 404 Not Found code. """ idp_id = uuid.uuid4().hex @@ -1178,12 +1265,13 @@ class FederatedIdentityProviderTests(FederationTests): def test_assign_protocol_to_idp(self): """Assign a protocol to existing IdP.""" - - self._assign_protocol_to_idp(expected_status=201) + self._assign_protocol_to_idp(expected_status=http_client.CREATED) def test_protocol_composite_pk(self): - """Test whether Keystone let's add two entities with identical - names, however attached to different IdPs. + """Test that Keystone can add two entities. + + The entities have identical names, however, attached to different + IdPs. 1. Add IdP and assign it protocol with predefined name 2. Add another IdP and assign it a protocol with same name. @@ -1193,7 +1281,7 @@ class FederatedIdentityProviderTests(FederationTests): """ url = self.base_url(suffix='%(idp_id)s/protocols/%(protocol_id)s') - kwargs = {'expected_status': 201} + kwargs = {'expected_status': http_client.CREATED} self._assign_protocol_to_idp(proto='saml2', url=url, **kwargs) @@ -1204,12 +1292,12 @@ class FederatedIdentityProviderTests(FederationTests): """Test whether Keystone checks for unique idp/protocol values. Add same protocol twice, expect Keystone to reject a latter call and - return HTTP 409 code. + return HTTP 409 Conflict code. """ url = self.base_url(suffix='%(idp_id)s/protocols/%(protocol_id)s') - kwargs = {'expected_status': 201} + kwargs = {'expected_status': http_client.CREATED} resp, idp_id, proto = self._assign_protocol_to_idp(proto='saml2', url=url, **kwargs) kwargs = {'expected_status': http_client.CONFLICT} @@ -1221,10 +1309,9 @@ class FederatedIdentityProviderTests(FederationTests): def test_assign_protocol_to_nonexistent_idp(self): """Assign protocol to IdP that doesn't exist. - Expect HTTP 404 code. + Expect HTTP 404 Not Found code. """ - idp_id = uuid.uuid4().hex kwargs = {'expected_status': http_client.NOT_FOUND} self._assign_protocol_to_idp(proto='saml2', @@ -1234,8 +1321,8 @@ class FederatedIdentityProviderTests(FederationTests): def test_get_protocol(self): """Create and later fetch protocol tied to IdP.""" - - resp, idp_id, proto = self._assign_protocol_to_idp(expected_status=201) + resp, idp_id, proto = self._assign_protocol_to_idp( + expected_status=http_client.CREATED) proto_id = self._fetch_attribute_from_response(resp, 'protocol')['id'] url = "%s/protocols/%s" % (idp_id, proto_id) url = self.base_url(suffix=url) @@ -1254,12 +1341,14 @@ class FederatedIdentityProviderTests(FederationTests): Compare input and output id sets. """ - resp, idp_id, proto = self._assign_protocol_to_idp(expected_status=201) + resp, idp_id, proto = self._assign_protocol_to_idp( + expected_status=http_client.CREATED) iterations = random.randint(0, 16) protocol_ids = [] for _ in range(iterations): - resp, _, proto = self._assign_protocol_to_idp(idp_id=idp_id, - expected_status=201) + resp, _, proto = self._assign_protocol_to_idp( + idp_id=idp_id, + expected_status=http_client.CREATED) proto_id = self._fetch_attribute_from_response(resp, 'protocol') proto_id = proto_id['id'] protocol_ids.append(proto_id) @@ -1277,8 +1366,8 @@ class FederatedIdentityProviderTests(FederationTests): def test_update_protocols_attribute(self): """Update protocol's attribute.""" - - resp, idp_id, proto = self._assign_protocol_to_idp(expected_status=201) + resp, idp_id, proto = self._assign_protocol_to_idp( + expected_status=http_client.CREATED) new_mapping_id = uuid.uuid4().hex url = "%s/protocols/%s" % (idp_id, proto) @@ -1294,19 +1383,21 @@ class FederatedIdentityProviderTests(FederationTests): def test_delete_protocol(self): """Delete protocol. - Expect HTTP 404 code for the GET call after the protocol is deleted. + Expect HTTP 404 Not Found code for the GET call after the protocol is + deleted. """ url = self.base_url(suffix='/%(idp_id)s/' 'protocols/%(protocol_id)s') - resp, idp_id, proto = self._assign_protocol_to_idp(expected_status=201) + resp, idp_id, proto = self._assign_protocol_to_idp( + expected_status=http_client.CREATED) url = url % {'idp_id': idp_id, 'protocol_id': proto} self.delete(url) self.get(url, expected_status=http_client.NOT_FOUND) -class MappingCRUDTests(FederationTests): +class MappingCRUDTests(test_v3.RestfulTestCase): """A class for testing CRUD operations for Mappings.""" MAPPING_URL = '/OS-FEDERATION/mappings/' @@ -1340,7 +1431,7 @@ class MappingCRUDTests(FederationTests): url = self.MAPPING_URL + uuid.uuid4().hex resp = self.put(url, body={'mapping': mapping_fixtures.MAPPING_LARGE}, - expected_status=201) + expected_status=http_client.CREATED) return resp def _get_id_from_response(self, resp): @@ -1357,7 +1448,7 @@ class MappingCRUDTests(FederationTests): resp = self.get(url) entities = resp.result.get('mappings') self.assertIsNotNone(entities) - self.assertResponseStatus(resp, 200) + self.assertResponseStatus(resp, http_client.OK) self.assertValidListLinks(resp.result.get('links')) self.assertEqual(1, len(entities)) @@ -1367,7 +1458,7 @@ class MappingCRUDTests(FederationTests): mapping_id = self._get_id_from_response(resp) url = url % {'mapping_id': str(mapping_id)} resp = self.delete(url) - self.assertResponseStatus(resp, 204) + self.assertResponseStatus(resp, http_client.NO_CONTENT) self.get(url, expected_status=http_client.NOT_FOUND) def test_mapping_get(self): @@ -1463,8 +1554,8 @@ class MappingCRUDTests(FederationTests): def test_create_mapping_with_blacklist_and_whitelist(self): """Test for adding whitelist and blacklist in the rule - Server should respond with HTTP 400 error upon discovering both - ``whitelist`` and ``blacklist`` keywords in the same rule. + Server should respond with HTTP 400 Bad Request error upon discovering + both ``whitelist`` and ``blacklist`` keywords in the same rule. """ url = self.MAPPING_URL + uuid.uuid4().hex @@ -1472,8 +1563,37 @@ class MappingCRUDTests(FederationTests): self.put(url, expected_status=http_client.BAD_REQUEST, body={'mapping': mapping}) + def test_create_mapping_with_local_user_and_local_domain(self): + url = self.MAPPING_URL + uuid.uuid4().hex + resp = self.put( + url, + body={ + 'mapping': mapping_fixtures.MAPPING_LOCAL_USER_LOCAL_DOMAIN + }, + expected_status=http_client.CREATED) + self.assertValidMappingResponse( + resp, mapping_fixtures.MAPPING_LOCAL_USER_LOCAL_DOMAIN) + + def test_create_mapping_with_ephemeral(self): + url = self.MAPPING_URL + uuid.uuid4().hex + resp = self.put( + url, + body={'mapping': mapping_fixtures.MAPPING_EPHEMERAL_USER}, + expected_status=http_client.CREATED) + self.assertValidMappingResponse( + resp, mapping_fixtures.MAPPING_EPHEMERAL_USER) + + def test_create_mapping_with_bad_user_type(self): + url = self.MAPPING_URL + uuid.uuid4().hex + # get a copy of a known good map + bad_mapping = copy.deepcopy(mapping_fixtures.MAPPING_EPHEMERAL_USER) + # now sabotage the user type + bad_mapping['rules'][0]['local'][0]['user']['type'] = uuid.uuid4().hex + self.put(url, expected_status=http_client.BAD_REQUEST, + body={'mapping': bad_mapping}) + -class FederatedTokenTests(FederationTests, FederatedSetupMixin): +class FederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin): def auth_plugin_config_override(self): methods = ['saml2'] @@ -1510,7 +1630,7 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): self.assertTrue(note['send_notification_called']) def load_fixtures(self, fixtures): - super(FederationTests, self).load_fixtures(fixtures) + super(FederatedTokenTests, self).load_fixtures(fixtures) self.load_federation_sample_data() def test_issue_unscoped_token_notify(self): @@ -1609,7 +1729,7 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): def test_issue_unscoped_token_with_remote_unavailable(self): self.config_fixture.config(group='federation', remote_id_attribute=self.REMOTE_ID_ATTR) - self.assertRaises(exception.ValidationError, + self.assertRaises(exception.Unauthorized, self._issue_unscoped_token, idp=self.IDP_WITH_REMOTE, environment={ @@ -1649,13 +1769,13 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): self.assertIsNotNone(r.headers.get('X-Subject-Token')) def test_scope_to_project_once_notify(self): - r = self.v3_authenticate_token( + r = self.v3_create_token( self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE) user_id = r.json['token']['user']['id'] self._assert_last_notify(self.ACTION, self.IDP, self.PROTOCOL, user_id) def test_scope_to_project_once(self): - r = self.v3_authenticate_token( + r = self.v3_create_token( self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE) token_resp = r.result['token'] project_id = token_resp['project']['id'] @@ -1685,14 +1805,13 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): """ enabled_false = {'enabled': False} self.federation_api.update_idp(self.IDP, enabled_false) - self.v3_authenticate_token( + self.v3_create_token( self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_CUSTOMER, expected_status=http_client.FORBIDDEN) def test_scope_to_bad_project(self): """Scope unscoped token with a project we don't have access to.""" - - self.v3_authenticate_token( + self.v3_create_token( self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_CUSTOMER, expected_status=http_client.UNAUTHORIZED) @@ -1705,13 +1824,12 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): * Employees' project """ - bodies = (self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_ADMIN, self.TOKEN_SCOPE_PROJECT_CUSTOMER_FROM_ADMIN) project_ids = (self.proj_employees['id'], self.proj_customers['id']) for body, project_id_ref in zip(bodies, project_ids): - r = self.v3_authenticate_token(body) + r = self.v3_create_token(body) token_resp = r.result['token'] self._check_project_scoped_token_attributes(token_resp, project_id_ref) @@ -1719,7 +1837,7 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): def test_scope_to_project_with_only_inherited_roles(self): """Try to scope token whose only roles are inherited.""" self.config_fixture.config(group='os_inherit', enabled=True) - r = self.v3_authenticate_token( + r = self.v3_create_token( self.TOKEN_SCOPE_PROJECT_INHERITED_FROM_CUSTOMER) token_resp = r.result['token'] self._check_project_scoped_token_attributes( @@ -1731,7 +1849,7 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): def test_scope_token_from_nonexistent_unscoped_token(self): """Try to scope token from non-existent unscoped token.""" - self.v3_authenticate_token( + self.v3_create_token( self.TOKEN_SCOPE_PROJECT_FROM_NONEXISTENT_TOKEN, expected_status=http_client.NOT_FOUND) @@ -1755,7 +1873,7 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): assertion='CONTRACTOR_ASSERTION') def test_scope_to_domain_once(self): - r = self.v3_authenticate_token(self.TOKEN_SCOPE_DOMAIN_A_FROM_CUSTOMER) + r = self.v3_create_token(self.TOKEN_SCOPE_DOMAIN_A_FROM_CUSTOMER) token_resp = r.result['token'] self._check_domain_scoped_token_attributes(token_resp, self.domainA['id']) @@ -1778,14 +1896,14 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): self.domainC['id']) for body, domain_id_ref in zip(bodies, domain_ids): - r = self.v3_authenticate_token(body) + r = self.v3_create_token(body) token_resp = r.result['token'] self._check_domain_scoped_token_attributes(token_resp, domain_id_ref) def test_scope_to_domain_with_only_inherited_roles_fails(self): """Try to scope to a domain that has no direct roles.""" - self.v3_authenticate_token( + self.v3_create_token( self.TOKEN_SCOPE_DOMAIN_D_FROM_CUSTOMER, expected_status=http_client.UNAUTHORIZED) @@ -1816,14 +1934,14 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): # TODO(samueldmq): Create another test class for role inheritance tests. # The advantage would be to reduce the complexity of this test class and - # have tests specific to this fuctionality grouped, easing readability and + # have tests specific to this functionality grouped, easing readability and # maintenability. def test_list_projects_for_inherited_project_assignment(self): # Enable os_inherit extension self.config_fixture.config(group='os_inherit', enabled=True) # Create a subproject - subproject_inherited = self.new_project_ref( + subproject_inherited = unit.new_project_ref( domain_id=self.domainD['id'], parent_id=self.project_inherited['id']) self.resource_api.create_project(subproject_inherited['id'], @@ -1878,6 +1996,9 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): self.assertEqual(domains_ref, domains, 'match failed for url %s' % url) + @utils.wip('This will fail because of bug #1501032. The returned method' + 'list should contain "saml2". This is documented in bug ' + '1501032.') def test_full_workflow(self): """Test 'standard' workflow for granting access tokens. @@ -1886,9 +2007,10 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): * Scope token to one of available projects """ - r = self._issue_unscoped_token() token_resp = r.json_body['token'] + # NOTE(lbragstad): Ensure only 'saml2' is in the method list. + self.assertListEqual(['saml2'], token_resp['methods']) self.assertValidMappedUser(token_resp) employee_unscoped_token_id = r.headers.get('X-Subject-Token') r = self.get('/auth/projects', token=employee_unscoped_token_id) @@ -1899,8 +2021,12 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): v3_scope_request = self._scope_request(employee_unscoped_token_id, 'project', project['id']) - r = self.v3_authenticate_token(v3_scope_request) + r = self.v3_create_token(v3_scope_request) token_resp = r.result['token'] + # FIXME(lbragstad): 'token' should be in the list of methods returned + # but it isn't. This is documented in bug 1501032. + self.assertIn('token', token_resp['methods']) + self.assertIn('saml2', token_resp['methods']) self._check_project_scoped_token_attributes(token_resp, project['id']) def test_workflow_with_groups_deletion(self): @@ -1917,10 +2043,9 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): """ # create group and role - group = self.new_group_ref( - domain_id=self.domainA['id']) + group = unit.new_group_ref(domain_id=self.domainA['id']) group = self.identity_api.create_group(group) - role = self.new_role_ref() + role = unit.new_role_ref() self.role_api.create_role(role['id'], role) # assign role to group and project_admins @@ -1971,7 +2096,8 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): token_id, 'project', self.project_all['id']) - self.v3_authenticate_token(scoped_token, expected_status=500) + self.v3_create_token( + scoped_token, expected_status=http_client.INTERNAL_SERVER_ERROR) def test_lists_with_missing_group_in_backend(self): """Test a mapping that points to a group that does not exist @@ -1990,8 +2116,7 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): """ domain_id = self.domainA['id'] domain_name = self.domainA['name'] - group = self.new_group_ref(domain_id=domain_id) - group['name'] = 'EXISTS' + group = unit.new_group_ref(domain_id=domain_id, name='EXISTS') group = self.identity_api.create_group(group) rules = { 'rules': [ @@ -2047,18 +2172,16 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): assigned """ - domain_id = self.domainA['id'] domain_name = self.domainA['name'] # Add a group "EXISTS" - group_exists = self.new_group_ref(domain_id=domain_id) - group_exists['name'] = 'EXISTS' + group_exists = unit.new_group_ref(domain_id=domain_id, name='EXISTS') group_exists = self.identity_api.create_group(group_exists) # Add a group "NO_EXISTS" - group_no_exists = self.new_group_ref(domain_id=domain_id) - group_no_exists['name'] = 'NO_EXISTS' + group_no_exists = unit.new_group_ref(domain_id=domain_id, + name='NO_EXISTS') group_no_exists = self.identity_api.create_group(group_no_exists) group_ids = set([group_exists['id'], group_no_exists['id']]) @@ -2122,18 +2245,17 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): assigned """ - domain_id = self.domainA['id'] domain_name = self.domainA['name'] # Add a group "EXISTS" - group_exists = self.new_group_ref(domain_id=domain_id) - group_exists['name'] = 'EXISTS' + group_exists = unit.new_group_ref(domain_id=domain_id, + name='EXISTS') group_exists = self.identity_api.create_group(group_exists) # Add a group "NO_EXISTS" - group_no_exists = self.new_group_ref(domain_id=domain_id) - group_no_exists['name'] = 'NO_EXISTS' + group_no_exists = unit.new_group_ref(domain_id=domain_id, + name='NO_EXISTS') group_no_exists = self.identity_api.create_group(group_no_exists) group_ids = set([group_exists['id'], group_no_exists['id']]) @@ -2198,8 +2320,7 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): """ domain_id = self.domainA['id'] domain_name = self.domainA['name'] - group = self.new_group_ref(domain_id=domain_id) - group['name'] = 'EXISTS' + group = unit.new_group_ref(domain_id=domain_id, name='EXISTS') group = self.identity_api.create_group(group) rules = { 'rules': [ @@ -2262,13 +2383,13 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): domain_name = self.domainA['name'] # Add a group "EXISTS" - group_exists = self.new_group_ref(domain_id=domain_id) - group_exists['name'] = 'EXISTS' + group_exists = unit.new_group_ref(domain_id=domain_id, + name='EXISTS') group_exists = self.identity_api.create_group(group_exists) # Add a group "NO_EXISTS" - group_no_exists = self.new_group_ref(domain_id=domain_id) - group_no_exists['name'] = 'NO_EXISTS' + group_no_exists = unit.new_group_ref(domain_id=domain_id, + name='NO_EXISTS') group_no_exists = self.identity_api.create_group(group_no_exists) group_ids = set([group_exists['id'], group_no_exists['id']]) @@ -2362,7 +2483,7 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): self._check_domains_are_valid(r.json_body['token']) def test_scoped_token_has_user_domain(self): - r = self.v3_authenticate_token( + r = self.v3_create_token( self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE) self._check_domains_are_valid(r.result['token']) @@ -2383,7 +2504,7 @@ class FederatedTokenTests(FederationTests, FederatedSetupMixin): assertion='ANOTHER_LOCAL_USER_ASSERTION') -class FernetFederatedTokenTests(FederationTests, FederatedSetupMixin): +class FernetFederatedTokenTests(test_v3.RestfulTestCase, FederatedSetupMixin): AUTH_METHOD = 'token' def load_fixtures(self, fixtures): @@ -2436,7 +2557,7 @@ class FernetFederatedTokenTests(FederationTests, FederatedSetupMixin): v3_scope_request = self._scope_request(unscoped_token, 'project', project['id']) - resp = self.v3_authenticate_token(v3_scope_request) + resp = self.v3_create_token(v3_scope_request) token_resp = resp.result['token'] self._check_project_scoped_token_attributes(token_resp, project['id']) @@ -2448,6 +2569,7 @@ class FederatedTokenTestsMethodToken(FederatedTokenTests): way for scoping all the tokens. """ + AUTH_METHOD = 'token' def auth_plugin_config_override(self): @@ -2455,8 +2577,67 @@ class FederatedTokenTestsMethodToken(FederatedTokenTests): super(FederatedTokenTests, self).auth_plugin_config_override(methods) + @utils.wip('This will fail because of bug #1501032. The returned method' + 'list should contain "saml2". This is documented in bug ' + '1501032.') + def test_full_workflow(self): + """Test 'standard' workflow for granting access tokens. + + * Issue unscoped token + * List available projects based on groups + * Scope token to one of available projects + + """ + r = self._issue_unscoped_token() + token_resp = r.json_body['token'] + # NOTE(lbragstad): Ensure only 'saml2' is in the method list. + self.assertListEqual(['saml2'], token_resp['methods']) + self.assertValidMappedUser(token_resp) + employee_unscoped_token_id = r.headers.get('X-Subject-Token') + r = self.get('/auth/projects', token=employee_unscoped_token_id) + projects = r.result['projects'] + random_project = random.randint(0, len(projects)) - 1 + project = projects[random_project] + + v3_scope_request = self._scope_request(employee_unscoped_token_id, + 'project', project['id']) + + r = self.v3_authenticate_token(v3_scope_request) + token_resp = r.result['token'] + self.assertIn('token', token_resp['methods']) + self.assertIn('saml2', token_resp['methods']) + self._check_project_scoped_token_attributes(token_resp, project['id']) + + +class FederatedUserTests(test_v3.RestfulTestCase, FederatedSetupMixin): + """Tests for federated users + + Tests new shadow users functionality + + """ + + def auth_plugin_config_override(self): + methods = ['saml2'] + super(FederatedUserTests, self).auth_plugin_config_override(methods) + + def setUp(self): + super(FederatedUserTests, self).setUp() + + def load_fixtures(self, fixtures): + super(FederatedUserTests, self).load_fixtures(fixtures) + self.load_federation_sample_data() + + def test_user_id_persistense(self): + """Ensure user_id is persistend for multiple federated authn calls.""" + r = self._issue_unscoped_token() + user_id = r.json_body['token']['user']['id'] -class JsonHomeTests(FederationTests, test_v3.JsonHomeTestMixin): + r = self._issue_unscoped_token() + user_id2 = r.json_body['token']['user']['id'] + self.assertEqual(user_id, user_id2) + + +class JsonHomeTests(test_v3.RestfulTestCase, test_v3.JsonHomeTestMixin): JSON_HOME_DATA = { 'http://docs.openstack.org/api/openstack-identity/3/ext/OS-FEDERATION/' '1.0/rel/identity_provider': { @@ -2484,7 +2665,7 @@ def _load_xml(filename): return xml.read() -class SAMLGenerationTests(FederationTests): +class SAMLGenerationTests(test_v3.RestfulTestCase): SP_AUTH_URL = ('http://beta.com:5000/v3/OS-FEDERATION/identity_providers' '/BETA/protocols/saml2/auth') @@ -2523,7 +2704,7 @@ class SAMLGenerationTests(FederationTests): self.sp = self.sp_ref() url = '/OS-FEDERATION/service_providers/' + self.SERVICE_PROVDIER_ID self.put(url, body={'service_provider': self.sp}, - expected_status=201) + expected_status=http_client.CREATED) def test_samlize_token_values(self): """Test the SAML generator produces a SAML object. @@ -2665,7 +2846,7 @@ class SAMLGenerationTests(FederationTests): """ if not _is_xmlsec1_installed(): - self.skip('xmlsec1 is not installed') + self.skipTest('xmlsec1 is not installed') generator = keystone_idp.SAMLGenerator() response = generator.samlize_token(self.ISSUER, self.RECIPIENT, @@ -2709,7 +2890,7 @@ class SAMLGenerationTests(FederationTests): user_id=self.user['id'], password=self.user['password'], project_id=self.project['id']) - resp = self.v3_authenticate_token(auth_data) + resp = self.v3_create_token(auth_data) token_id = resp.headers.get('X-Subject-Token') return token_id @@ -2718,7 +2899,7 @@ class SAMLGenerationTests(FederationTests): user_id=self.user['id'], password=self.user['password'], user_domain_id=self.domain['id']) - resp = self.v3_authenticate_token(auth_data) + resp = self.v3_create_token(auth_data) token_id = resp.headers.get('X-Subject-Token') return token_id @@ -2757,7 +2938,7 @@ class SAMLGenerationTests(FederationTests): return_value=self.signed_assertion): http_response = self.post(self.SAML_GENERATION_ROUTE, body=body, response_content_type='text/xml', - expected_status=200) + expected_status=http_client.OK) response = etree.fromstring(http_response.result) issuer = response[0] @@ -2789,10 +2970,9 @@ class SAMLGenerationTests(FederationTests): def test_invalid_scope_body(self): """Test that missing the scope in request body raises an exception. - Raises exception.SchemaValidationError() - error code 400 + Raises exception.SchemaValidationError() - error 400 Bad Request """ - token_id = uuid.uuid4().hex body = self._create_generate_saml_request(token_id, self.SERVICE_PROVDIER_ID) @@ -2804,10 +2984,9 @@ class SAMLGenerationTests(FederationTests): def test_invalid_token_body(self): """Test that missing the token in request body raises an exception. - Raises exception.SchemaValidationError() - error code 400 + Raises exception.SchemaValidationError() - error 400 Bad Request """ - token_id = uuid.uuid4().hex body = self._create_generate_saml_request(token_id, self.SERVICE_PROVDIER_ID) @@ -2819,7 +2998,7 @@ class SAMLGenerationTests(FederationTests): def test_sp_not_found(self): """Test SAML generation with an invalid service provider ID. - Raises exception.ServiceProviderNotFound() - error code 404 + Raises exception.ServiceProviderNotFound() - error Not Found 404 """ sp_id = uuid.uuid4().hex @@ -2830,7 +3009,6 @@ class SAMLGenerationTests(FederationTests): def test_sp_disabled(self): """Try generating assertion for disabled Service Provider.""" - # Disable Service Provider sp_ref = {'enabled': False} self.federation_api.update_sp(self.SERVICE_PROVDIER_ID, sp_ref) @@ -2844,10 +3022,9 @@ class SAMLGenerationTests(FederationTests): def test_token_not_found(self): """Test that an invalid token in the request body raises an exception. - Raises exception.TokenNotFound() - error code 404 + Raises exception.TokenNotFound() - error Not Found 404 """ - token_id = uuid.uuid4().hex body = self._create_generate_saml_request(token_id, self.SERVICE_PROVDIER_ID) @@ -2863,7 +3040,6 @@ class SAMLGenerationTests(FederationTests): The controller should return a SAML assertion that is wrapped in a SOAP envelope. """ - self.config_fixture.config(group='saml', idp_entity_id=self.ISSUER) token_id = self._fetch_valid_token() body = self._create_generate_saml_request(token_id, @@ -2873,7 +3049,7 @@ class SAMLGenerationTests(FederationTests): return_value=self.signed_assertion): http_response = self.post(self.ECP_GENERATION_ROUTE, body=body, response_content_type='text/xml', - expected_status=200) + expected_status=http_client.OK) env_response = etree.fromstring(http_response.result) header = env_response[0] @@ -2956,7 +3132,7 @@ class SAMLGenerationTests(FederationTests): self.assertEqual(expected_log, logger_fixture.output) -class IdPMetadataGenerationTests(FederationTests): +class IdPMetadataGenerationTests(test_v3.RestfulTestCase): """A class for testing Identity Provider Metadata generation.""" METADATA_URL = '/OS-FEDERATION/saml2/metadata' @@ -3073,20 +3249,20 @@ class IdPMetadataGenerationTests(FederationTests): self.generator.generate_metadata) def test_get_metadata_with_no_metadata_file_configured(self): - self.get(self.METADATA_URL, expected_status=500) + self.get(self.METADATA_URL, + expected_status=http_client.INTERNAL_SERVER_ERROR) def test_get_metadata(self): self.config_fixture.config( group='saml', idp_metadata_path=XMLDIR + '/idp_saml2_metadata.xml') - r = self.get(self.METADATA_URL, response_content_type='text/xml', - expected_status=200) + r = self.get(self.METADATA_URL, response_content_type='text/xml') self.assertEqual('text/xml', r.headers.get('Content-Type')) reference_file = _load_xml('idp_saml2_metadata.xml') self.assertEqual(reference_file, r.result) -class ServiceProviderTests(FederationTests): +class ServiceProviderTests(test_v3.RestfulTestCase): """A test class for Service Providers.""" MEMBER_NAME = 'service_provider' @@ -3096,13 +3272,13 @@ class ServiceProviderTests(FederationTests): 'relay_state_prefix', 'sp_url'] def setUp(self): - super(FederationTests, self).setUp() + super(ServiceProviderTests, self).setUp() # Add a Service Provider url = self.base_url(suffix=self.SERVICE_PROVIDER_ID) self.SP_REF = self.sp_ref() self.SERVICE_PROVIDER = self.put( url, body={'service_provider': self.SP_REF}, - expected_status=201).result + expected_status=http_client.CREATED).result def sp_ref(self): ref = { @@ -3119,9 +3295,18 @@ class ServiceProviderTests(FederationTests): return '/OS-FEDERATION/service_providers/' + str(suffix) return '/OS-FEDERATION/service_providers' + def _create_default_sp(self, body=None): + """Create default Service Provider.""" + url = self.base_url(suffix=uuid.uuid4().hex) + if body is None: + body = self.sp_ref() + resp = self.put(url, body={'service_provider': body}, + expected_status=http_client.CREATED) + return resp + def test_get_service_provider(self): url = self.base_url(suffix=self.SERVICE_PROVIDER_ID) - resp = self.get(url, expected_status=200) + resp = self.get(url) self.assertValidEntity(resp.result['service_provider'], keys_to_check=self.SP_KEYS) @@ -3133,7 +3318,7 @@ class ServiceProviderTests(FederationTests): url = self.base_url(suffix=uuid.uuid4().hex) sp = self.sp_ref() resp = self.put(url, body={'service_provider': sp}, - expected_status=201) + expected_status=http_client.CREATED) self.assertValidEntity(resp.result['service_provider'], keys_to_check=self.SP_KEYS) @@ -3143,7 +3328,7 @@ class ServiceProviderTests(FederationTests): sp = self.sp_ref() del sp['relay_state_prefix'] resp = self.put(url, body={'service_provider': sp}, - expected_status=201) + expected_status=http_client.CREATED) sp_result = resp.result['service_provider'] self.assertEqual(CONF.saml.relay_state_prefix, sp_result['relay_state_prefix']) @@ -3155,7 +3340,7 @@ class ServiceProviderTests(FederationTests): non_default_prefix = uuid.uuid4().hex sp['relay_state_prefix'] = non_default_prefix resp = self.put(url, body={'service_provider': sp}, - expected_status=201) + expected_status=http_client.CREATED) sp_result = resp.result['service_provider'] self.assertEqual(non_default_prefix, sp_result['relay_state_prefix']) @@ -3182,7 +3367,8 @@ class ServiceProviderTests(FederationTests): } for id, sp in ref_service_providers.items(): url = self.base_url(suffix=id) - self.put(url, body={'service_provider': sp}, expected_status=201) + self.put(url, body={'service_provider': sp}, + expected_status=http_client.CREATED) # Insert ids into service provider object, we will compare it with # responses from server and those include 'id' attribute. @@ -3209,15 +3395,14 @@ class ServiceProviderTests(FederationTests): """ new_sp_ref = self.sp_ref() url = self.base_url(suffix=self.SERVICE_PROVIDER_ID) - resp = self.patch(url, body={'service_provider': new_sp_ref}, - expected_status=200) + resp = self.patch(url, body={'service_provider': new_sp_ref}) patch_result = resp.result new_sp_ref['id'] = self.SERVICE_PROVIDER_ID self.assertValidEntity(patch_result['service_provider'], ref=new_sp_ref, keys_to_check=self.SP_KEYS) - resp = self.get(url, expected_status=200) + resp = self.get(url) get_result = resp.result self.assertDictEqual(patch_result['service_provider'], @@ -3227,7 +3412,7 @@ class ServiceProviderTests(FederationTests): """Update immutable attributes in service provider. In this particular case the test will try to change ``id`` attribute. - The server should return an HTTP 403 error code. + The server should return an HTTP 403 Forbidden error code. """ new_sp_ref = {'id': uuid.uuid4().hex} @@ -3242,7 +3427,7 @@ class ServiceProviderTests(FederationTests): self.patch(url, body={'service_provider': new_sp_ref}, expected_status=http_client.BAD_REQUEST) - def test_update_service_provider_404(self): + def test_update_service_provider_returns_not_found(self): new_sp_ref = self.sp_ref() new_sp_ref['description'] = uuid.uuid4().hex url = self.base_url(suffix=uuid.uuid4().hex) @@ -3250,25 +3435,74 @@ class ServiceProviderTests(FederationTests): expected_status=http_client.NOT_FOUND) def test_update_sp_relay_state(self): - """Update an SP with custome relay state.""" + """Update an SP with custom relay state.""" new_sp_ref = self.sp_ref() non_default_prefix = uuid.uuid4().hex new_sp_ref['relay_state_prefix'] = non_default_prefix url = self.base_url(suffix=self.SERVICE_PROVIDER_ID) - resp = self.patch(url, body={'service_provider': new_sp_ref}, - expected_status=200) + resp = self.patch(url, body={'service_provider': new_sp_ref}) sp_result = resp.result['service_provider'] self.assertEqual(non_default_prefix, sp_result['relay_state_prefix']) def test_delete_service_provider(self): url = self.base_url(suffix=self.SERVICE_PROVIDER_ID) - self.delete(url, expected_status=204) + self.delete(url) - def test_delete_service_provider_404(self): + def test_delete_service_provider_returns_not_found(self): url = self.base_url(suffix=uuid.uuid4().hex) self.delete(url, expected_status=http_client.NOT_FOUND) + def test_filter_list_sp_by_id(self): + def get_id(resp): + sp = resp.result.get('service_provider') + return sp.get('id') + + sp1_id = get_id(self._create_default_sp()) + sp2_id = get_id(self._create_default_sp()) + + # list the SP, should get SPs. + url = self.base_url() + resp = self.get(url) + sps = resp.result.get('service_providers') + entities_ids = [e['id'] for e in sps] + self.assertIn(sp1_id, entities_ids) + self.assertIn(sp2_id, entities_ids) + + # filter the SP by 'id'. Only SP1 should appear. + url = self.base_url() + '?id=' + sp1_id + resp = self.get(url) + sps = resp.result.get('service_providers') + entities_ids = [e['id'] for e in sps] + self.assertIn(sp1_id, entities_ids) + self.assertNotIn(sp2_id, entities_ids) + + def test_filter_list_sp_by_enabled(self): + def get_id(resp): + sp = resp.result.get('service_provider') + return sp.get('id') + + sp1_id = get_id(self._create_default_sp()) + sp2_ref = self.sp_ref() + sp2_ref['enabled'] = False + sp2_id = get_id(self._create_default_sp(body=sp2_ref)) + + # list the SP, should get two SPs. + url = self.base_url() + resp = self.get(url) + sps = resp.result.get('service_providers') + entities_ids = [e['id'] for e in sps] + self.assertIn(sp1_id, entities_ids) + self.assertIn(sp2_id, entities_ids) + + # filter the SP by 'enabled'. Only SP1 should appear. + url = self.base_url() + '?enabled=True' + resp = self.get(url) + sps = resp.result.get('service_providers') + entities_ids = [e['id'] for e in sps] + self.assertIn(sp1_id, entities_ids) + self.assertNotIn(sp2_id, entities_ids) + class WebSSOTests(FederatedTokenTests): """A class for testing Web SSO.""" @@ -3306,6 +3540,21 @@ class WebSSOTests(FederatedTokenTests): resp = self.api.federated_sso_auth(context, self.PROTOCOL) self.assertIn(self.TRUSTED_DASHBOARD, resp.body) + def test_get_sso_origin_host_case_insensitive(self): + # test lowercase hostname in trusted_dashboard + context = { + 'query_string': { + 'origin': "http://horizon.com", + }, + } + host = self.api._get_sso_origin_host(context) + self.assertEqual("http://horizon.com", host) + # test uppercase hostname in trusted_dashboard + self.config_fixture.config(group='federation', + trusted_dashboard=['http://Horizon.com']) + host = self.api._get_sso_origin_host(context) + self.assertEqual("http://horizon.com", host) + def test_federated_sso_auth_with_protocol_specific_remote_id(self): self.config_fixture.config( group=self.PROTOCOL, @@ -3380,7 +3629,7 @@ class WebSSOTests(FederatedTokenTests): self.assertIn(self.TRUSTED_DASHBOARD, resp.body) -class K2KServiceCatalogTests(FederationTests): +class K2KServiceCatalogTests(test_v3.RestfulTestCase): SP1 = 'SP1' SP2 = 'SP2' SP3 = 'SP3' @@ -3429,11 +3678,10 @@ class K2KServiceCatalogTests(FederationTests): for entity in service_providers: id = entity.get('id') ref_entity = self.sp_response(id, ref.get(id)) - self.assertDictEqual(ref_entity, entity) + self.assertDictEqual(entity, ref_entity) def test_service_providers_in_token(self): """Check if service providers are listed in service catalog.""" - token = self.token_v3_helper.get_token_data(self.user_id, ['password']) ref = {} for r in (self.sp_alpha, self.sp_beta, self.sp_gamma): diff --git a/keystone-moon/keystone/tests/unit/test_v3_filters.py b/keystone-moon/keystone/tests/unit/test_v3_filters.py index 668a2308..9dc19af5 100644 --- a/keystone-moon/keystone/tests/unit/test_v3_filters.py +++ b/keystone-moon/keystone/tests/unit/test_v3_filters.py @@ -13,13 +13,13 @@ # License for the specific language governing permissions and limitations # under the License. -import uuid - from oslo_config import cfg from oslo_serialization import jsonutils from six.moves import range +from keystone.tests import unit from keystone.tests.unit import filtering +from keystone.tests.unit import ksfixtures from keystone.tests.unit.ksfixtures import temporaryfile from keystone.tests.unit import test_v3 @@ -31,14 +31,14 @@ class IdentityTestFilteredCase(filtering.FilterTests, test_v3.RestfulTestCase): """Test filter enforcement on the v3 Identity API.""" + def _policy_fixture(self): + return ksfixtures.Policy(self.tmpfilename, self.config_fixture) + def setUp(self): """Setup for Identity Filter Test Cases.""" - - super(IdentityTestFilteredCase, self).setUp() self.tempfile = self.useFixture(temporaryfile.SecureTempFile()) self.tmpfilename = self.tempfile.file_name - self.config_fixture.config(group='oslo_policy', - policy_file=self.tmpfilename) + super(IdentityTestFilteredCase, self).setUp() def load_sample_data(self): """Create sample data for these tests. @@ -57,32 +57,23 @@ class IdentityTestFilteredCase(filtering.FilterTests, """ # Start by creating a few domains self._populate_default_domain() - self.domainA = self.new_domain_ref() + self.domainA = unit.new_domain_ref() self.resource_api.create_domain(self.domainA['id'], self.domainA) - self.domainB = self.new_domain_ref() + self.domainB = unit.new_domain_ref() self.resource_api.create_domain(self.domainB['id'], self.domainB) - self.domainC = self.new_domain_ref() + self.domainC = unit.new_domain_ref() self.domainC['enabled'] = False self.resource_api.create_domain(self.domainC['id'], self.domainC) # Now create some users, one in domainA and two of them in domainB - self.user1 = self.new_user_ref(domain_id=self.domainA['id']) - password = uuid.uuid4().hex - self.user1['password'] = password - self.user1 = self.identity_api.create_user(self.user1) - self.user1['password'] = password - - self.user2 = self.new_user_ref(domain_id=self.domainB['id']) - self.user2['password'] = password - self.user2 = self.identity_api.create_user(self.user2) - self.user2['password'] = password - - self.user3 = self.new_user_ref(domain_id=self.domainB['id']) - self.user3['password'] = password - self.user3 = self.identity_api.create_user(self.user3) - self.user3['password'] = password - - self.role = self.new_role_ref() + self.user1 = unit.create_user(self.identity_api, + domain_id=self.domainA['id']) + self.user2 = unit.create_user(self.identity_api, + domain_id=self.domainB['id']) + self.user3 = unit.create_user(self.identity_api, + domain_id=self.domainB['id']) + + self.role = unit.new_role_ref() self.role_api.create_role(self.role['id'], self.role) self.assignment_api.create_grant(self.role['id'], user_id=self.user1['id'], @@ -311,7 +302,7 @@ class IdentityTestFilteredCase(filtering.FilterTests, # See if we can add a SQL command...use the group table instead of the # user table since 'user' is reserved word for SQLAlchemy. - group = self.new_group_ref(domain_id=self.domainB['id']) + group = unit.new_group_ref(domain_id=self.domainB['id']) group = self.identity_api.create_group(group) url_by_name = "/users?name=x'; drop table group" @@ -325,11 +316,11 @@ class IdentityTestFilteredCase(filtering.FilterTests, class IdentityTestListLimitCase(IdentityTestFilteredCase): """Test list limiting enforcement on the v3 Identity API.""" + content_type = 'json' def setUp(self): """Setup for Identity Limit Test Cases.""" - super(IdentityTestListLimitCase, self).setUp() # Create 10 entries for each of the entities we are going to test @@ -343,7 +334,7 @@ class IdentityTestListLimitCase(IdentityTestFilteredCase): self.service_list = [] self.addCleanup(self.clean_up_service) for _ in range(10): - new_entity = {'id': uuid.uuid4().hex, 'type': uuid.uuid4().hex} + new_entity = unit.new_service_ref() service = self.catalog_api.create_service(new_entity['id'], new_entity) self.service_list.append(service) @@ -351,26 +342,22 @@ class IdentityTestListLimitCase(IdentityTestFilteredCase): self.policy_list = [] self.addCleanup(self.clean_up_policy) for _ in range(10): - new_entity = {'id': uuid.uuid4().hex, 'type': uuid.uuid4().hex, - 'blob': uuid.uuid4().hex} + new_entity = unit.new_policy_ref() policy = self.policy_api.create_policy(new_entity['id'], new_entity) self.policy_list.append(policy) def clean_up_entity(self, entity): """Clean up entity test data from Identity Limit Test Cases.""" - self._delete_test_data(entity, self.entity_lists[entity]) def clean_up_service(self): """Clean up service test data from Identity Limit Test Cases.""" - for service in self.service_list: self.catalog_api.delete_service(service['id']) def clean_up_policy(self): """Clean up policy test data from Identity Limit Test Cases.""" - for policy in self.policy_list: self.policy_api.delete_policy(policy['id']) @@ -430,7 +417,6 @@ class IdentityTestListLimitCase(IdentityTestFilteredCase): def test_no_limit(self): """Check truncated attribute not set when list not limited.""" - self._set_policy({"identity:list_services": []}) r = self.get('/services', auth=self.auth) self.assertEqual(10, len(r.result.get('services'))) @@ -438,7 +424,6 @@ class IdentityTestListLimitCase(IdentityTestFilteredCase): def test_at_limit(self): """Check truncated attribute not set when list at max size.""" - # Test this by overriding the general limit with a higher # driver-specific limit (allowing all entities to be returned # in the collection), which should result in a non truncated list diff --git a/keystone-moon/keystone/tests/unit/test_v3_identity.py b/keystone-moon/keystone/tests/unit/test_v3_identity.py index 5a8e4fd5..7d3f6cad 100644 --- a/keystone-moon/keystone/tests/unit/test_v3_identity.py +++ b/keystone-moon/keystone/tests/unit/test_v3_identity.py @@ -30,31 +30,63 @@ from keystone.tests.unit import test_v3 CONF = cfg.CONF +# NOTE(morganfainberg): To be removed when admin_token_auth middleware is +# removed. This was moved to it's own testcase so it can setup the +# admin_token_auth pipeline without impacting other tests. +class IdentityTestCaseStaticAdminToken(test_v3.RestfulTestCase): + EXTENSION_TO_ADD = 'admin_token_auth' + + def config_overrides(self): + super(IdentityTestCaseStaticAdminToken, self).config_overrides() + self.config_fixture.config( + admin_token='ADMIN') + + def test_list_users_with_static_admin_token_and_multiple_backends(self): + # domain-specific operations with the bootstrap ADMIN token is + # disallowed when domain-specific drivers are enabled + self.config_fixture.config(group='identity', + domain_specific_drivers_enabled=True) + self.get('/users', token=CONF.admin_token, + expected_status=exception.Unauthorized.code) + + def test_create_user_with_admin_token_and_no_domain(self): + """Call ``POST /users`` with admin token but no domain id. + + It should not be possible to use the admin token to create a user + while not explicitly passing the domain in the request body. + + """ + # Passing a valid domain id to new_user_ref() since domain_id is + # not an optional parameter. + ref = unit.new_user_ref(domain_id=self.domain_id) + # Delete the domain id before sending the request. + del ref['domain_id'] + self.post('/users', body={'user': ref}, token=CONF.admin_token, + expected_status=http_client.BAD_REQUEST) + + class IdentityTestCase(test_v3.RestfulTestCase): """Test users and groups.""" def setUp(self): super(IdentityTestCase, self).setUp() - self.group = self.new_group_ref( - domain_id=self.domain_id) + self.group = unit.new_group_ref(domain_id=self.domain_id) self.group = self.identity_api.create_group(self.group) self.group_id = self.group['id'] - self.credential_id = uuid.uuid4().hex - self.credential = self.new_credential_ref( + self.credential = unit.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) + + self.credential_api.create_credential(self.credential['id'], + self.credential) # user crud tests def test_create_user(self): """Call ``POST /users``.""" - ref = self.new_user_ref(domain_id=self.domain_id) + ref = unit.new_user_ref(domain_id=self.domain_id) r = self.post( '/users', body={'user': ref}) @@ -70,17 +102,14 @@ class IdentityTestCase(test_v3.RestfulTestCase): """ # Create a user with a role on the domain so we can get a # domain scoped token - domain = self.new_domain_ref() + domain = unit.new_domain_ref() self.resource_api.create_domain(domain['id'], domain) - user = self.new_user_ref(domain_id=domain['id']) - password = user['password'] - user = self.identity_api.create_user(user) - user['password'] = password + user = unit.create_user(self.identity_api, domain_id=domain['id']) self.assignment_api.create_grant( role_id=self.role_id, user_id=user['id'], domain_id=domain['id']) - ref = self.new_user_ref(domain_id=domain['id']) + ref = unit.new_user_ref(domain_id=domain['id']) ref_nd = ref.copy() ref_nd.pop('domain_id') auth = self.build_authentication_request( @@ -91,7 +120,7 @@ class IdentityTestCase(test_v3.RestfulTestCase): self.assertValidUserResponse(r, ref) # Now try the same thing without a domain token - which should fail - ref = self.new_user_ref(domain_id=domain['id']) + ref = unit.new_user_ref(domain_id=domain['id']) ref_nd = ref.copy() ref_nd.pop('domain_id') auth = self.build_authentication_request( @@ -112,6 +141,79 @@ class IdentityTestCase(test_v3.RestfulTestCase): ref['domain_id'] = CONF.identity.default_domain_id return self.assertValidUserResponse(r, ref) + def test_create_user_with_admin_token_and_domain(self): + """Call ``POST /users`` with admin token and domain id.""" + ref = unit.new_user_ref(domain_id=self.domain_id) + self.post('/users', body={'user': ref}, token=self.get_admin_token(), + expected_status=http_client.CREATED) + + def test_user_management_normalized_keys(self): + """Illustrate the inconsistent handling of hyphens in keys. + + To quote Morgan in bug 1526244: + + the reason this is converted from "domain-id" to "domain_id" is + because of how we process/normalize data. The way we have to handle + specific data types for known columns requires avoiding "-" in the + actual python code since "-" is not valid for attributes in python + w/o significant use of "getattr" etc. + + In short, historically we handle some things in conversions. The + use of "extras" has long been a poor design choice that leads to + odd/strange inconsistent behaviors because of other choices made in + handling data from within the body. (In many cases we convert from + "-" to "_" throughout openstack) + + Source: https://bugs.launchpad.net/keystone/+bug/1526244/comments/9 + + """ + # Create two domains to work with. + domain1 = unit.new_domain_ref() + self.resource_api.create_domain(domain1['id'], domain1) + domain2 = unit.new_domain_ref() + self.resource_api.create_domain(domain2['id'], domain2) + + # We can successfully create a normal user without any surprises. + user = unit.new_user_ref(domain_id=domain1['id']) + r = self.post( + '/users', + body={'user': user}) + self.assertValidUserResponse(r, user) + user['id'] = r.json['user']['id'] + + # Query strings are not normalized: so we get all users back (like + # self.user), not just the ones in the specified domain. + r = self.get( + '/users?domain-id=%s' % domain1['id']) + self.assertValidUserListResponse(r, ref=self.user) + self.assertNotEqual(domain1['id'], self.user['domain_id']) + + # When creating a new user, if we move the 'domain_id' into the + # 'domain-id' attribute, the server will normalize the request + # attribute, and effectively "move it back" for us. + user = unit.new_user_ref(domain_id=domain1['id']) + user['domain-id'] = user.pop('domain_id') + r = self.post( + '/users', + body={'user': user}) + self.assertNotIn('domain-id', r.json['user']) + self.assertEqual(domain1['id'], r.json['user']['domain_id']) + # (move this attribute back so we can use assertValidUserResponse) + user['domain_id'] = user.pop('domain-id') + self.assertValidUserResponse(r, user) + user['id'] = r.json['user']['id'] + + # If we try updating the user's 'domain_id' by specifying a + # 'domain-id', then it'll be stored into extras rather than normalized, + # and the user's actual 'domain_id' is not affected. + r = self.patch( + '/users/%s' % user['id'], + body={'user': {'domain-id': domain2['id']}}) + self.assertEqual(domain2['id'], r.json['user']['domain-id']) + self.assertEqual(user['domain_id'], r.json['user']['domain_id']) + self.assertNotEqual(domain2['id'], user['domain_id']) + self.assertValidUserResponse(r, user) + def test_create_user_bad_request(self): """Call ``POST /users``.""" self.post('/users', body={'user': {}}, @@ -134,29 +236,42 @@ class IdentityTestCase(test_v3.RestfulTestCase): self.config_fixture.config(group='identity', domain_specific_drivers_enabled=True) - # Create a user with a role on the domain so we can get a - # domain scoped token - domain = self.new_domain_ref() + # Create a new domain with a new project and user + domain = unit.new_domain_ref() self.resource_api.create_domain(domain['id'], domain) - user = self.new_user_ref(domain_id=domain['id']) - password = user['password'] - user = self.identity_api.create_user(user) - user['password'] = password + + project = unit.new_project_ref(domain_id=domain['id']) + self.resource_api.create_project(project['id'], project) + + user = unit.create_user(self.identity_api, domain_id=domain['id']) + + # Create both project and domain role grants for the user so we + # can get both project and domain scoped tokens self.assignment_api.create_grant( role_id=self.role_id, user_id=user['id'], domain_id=domain['id']) + self.assignment_api.create_grant( + role_id=self.role_id, user_id=user['id'], + project_id=project['id']) - ref = self.new_user_ref(domain_id=domain['id']) - ref_nd = ref.copy() - ref_nd.pop('domain_id') - auth = self.build_authentication_request( + dom_auth = self.build_authentication_request( user_id=user['id'], password=user['password'], domain_id=domain['id']) + project_auth = self.build_authentication_request( + user_id=user['id'], + password=user['password'], + project_id=project['id']) # First try using a domain scoped token resource_url = '/users' - r = self.get(resource_url, auth=auth) + r = self.get(resource_url, auth=dom_auth) + self.assertValidUserListResponse(r, ref=user, + resource_url=resource_url) + + # Now try using a project scoped token + resource_url = '/users' + r = self.get(resource_url, auth=project_auth) self.assertValidUserListResponse(r, ref=user, resource_url=resource_url) @@ -167,21 +282,9 @@ class IdentityTestCase(test_v3.RestfulTestCase): self.assertValidUserListResponse(r, ref=user, resource_url=resource_url) - # Now try the same thing without a domain token or filter, - # which should fail - r = self.get('/users', expected_status=exception.Unauthorized.code) - - def test_list_users_with_static_admin_token_and_multiple_backends(self): - # domain-specific operations with the bootstrap ADMIN token is - # disallowed when domain-specific drivers are enabled - self.config_fixture.config(group='identity', - domain_specific_drivers_enabled=True) - self.get('/users', token=CONF.admin_token, - expected_status=exception.Unauthorized.code) - def test_list_users_no_default_project(self): """Call ``GET /users`` making sure no default_project_id.""" - user = self.new_user_ref(self.domain_id) + user = unit.new_user_ref(self.domain_id) user = self.identity_api.create_user(user) resource_url = '/users' r = self.get(resource_url) @@ -196,7 +299,7 @@ class IdentityTestCase(test_v3.RestfulTestCase): def test_get_user_with_default_project(self): """Call ``GET /users/{user_id}`` making sure of default_project_id.""" - user = self.new_user_ref(domain_id=self.domain_id, + user = unit.new_user_ref(domain_id=self.domain_id, project_id=self.project_id) user = self.identity_api.create_user(user) r = self.get('/users/%(user_id)s' % {'user_id': user['id']}) @@ -209,45 +312,39 @@ class IdentityTestCase(test_v3.RestfulTestCase): def test_list_groups_for_user(self): """Call ``GET /users/{user_id}/groups``.""" + user1 = unit.create_user(self.identity_api, + domain_id=self.domain['id']) + user2 = unit.create_user(self.identity_api, + domain_id=self.domain['id']) - self.user1 = self.new_user_ref( - domain_id=self.domain['id']) - password = self.user1['password'] - self.user1 = self.identity_api.create_user(self.user1) - self.user1['password'] = password - self.user2 = self.new_user_ref( - domain_id=self.domain['id']) - password = self.user2['password'] - self.user2 = self.identity_api.create_user(self.user2) - self.user2['password'] = password self.put('/groups/%(group_id)s/users/%(user_id)s' % { - 'group_id': self.group_id, 'user_id': self.user1['id']}) + 'group_id': self.group_id, 'user_id': user1['id']}) # Scenarios below are written to test the default policy configuration # One should be allowed to list one's own groups auth = self.build_authentication_request( - user_id=self.user1['id'], - password=self.user1['password']) + user_id=user1['id'], + password=user1['password']) resource_url = ('/users/%(user_id)s/groups' % - {'user_id': self.user1['id']}) + {'user_id': user1['id']}) r = self.get(resource_url, auth=auth) self.assertValidGroupListResponse(r, ref=self.group, resource_url=resource_url) # Administrator is allowed to list others' groups resource_url = ('/users/%(user_id)s/groups' % - {'user_id': self.user1['id']}) + {'user_id': user1['id']}) r = self.get(resource_url) self.assertValidGroupListResponse(r, ref=self.group, resource_url=resource_url) # Ordinary users should not be allowed to list other's groups auth = self.build_authentication_request( - user_id=self.user2['id'], - password=self.user2['password']) + user_id=user2['id'], + password=user2['password']) r = self.get('/users/%(user_id)s/groups' % { - 'user_id': self.user1['id']}, auth=auth, + 'user_id': user1['id']}, auth=auth, expected_status=exception.ForbiddenAction.code) def test_check_user_in_group(self): @@ -278,7 +375,7 @@ class IdentityTestCase(test_v3.RestfulTestCase): def test_update_user(self): """Call ``PATCH /users/{user_id}``.""" - user = self.new_user_ref(domain_id=self.domain_id) + user = unit.new_user_ref(domain_id=self.domain_id) del user['id'] r = self.patch('/users/%(user_id)s' % { 'user_id': self.user['id']}, @@ -287,44 +384,42 @@ class IdentityTestCase(test_v3.RestfulTestCase): def test_admin_password_reset(self): # bootstrap a user as admin - user_ref = self.new_user_ref(domain_id=self.domain['id']) - password = user_ref['password'] - user_ref = self.identity_api.create_user(user_ref) + user_ref = unit.create_user(self.identity_api, + domain_id=self.domain['id']) # auth as user should work before a password change old_password_auth = self.build_authentication_request( user_id=user_ref['id'], - password=password) - r = self.v3_authenticate_token(old_password_auth, expected_status=201) + password=user_ref['password']) + r = self.v3_create_token(old_password_auth) old_token = r.headers.get('X-Subject-Token') # auth as user with a token should work before a password change old_token_auth = self.build_authentication_request(token=old_token) - self.v3_authenticate_token(old_token_auth, expected_status=201) + self.v3_create_token(old_token_auth) # administrative password reset new_password = uuid.uuid4().hex self.patch('/users/%s' % user_ref['id'], - body={'user': {'password': new_password}}, - expected_status=200) + body={'user': {'password': new_password}}) # auth as user with original password should not work after change - self.v3_authenticate_token(old_password_auth, - expected_status=http_client.UNAUTHORIZED) + self.v3_create_token(old_password_auth, + expected_status=http_client.UNAUTHORIZED) # auth as user with an old token should not work after change - self.v3_authenticate_token(old_token_auth, - expected_status=http_client.NOT_FOUND) + self.v3_create_token(old_token_auth, + expected_status=http_client.NOT_FOUND) # new password should work new_password_auth = self.build_authentication_request( user_id=user_ref['id'], password=new_password) - self.v3_authenticate_token(new_password_auth, expected_status=201) + self.v3_create_token(new_password_auth) def test_update_user_domain_id(self): """Call ``PATCH /users/{user_id}`` with domain_id.""" - user = self.new_user_ref(domain_id=self.domain['id']) + user = unit.new_user_ref(domain_id=self.domain['id']) user = self.identity_api.create_user(user) user['domain_id'] = CONF.identity.default_domain_id r = self.patch('/users/%(user_id)s' % { @@ -349,18 +444,16 @@ class IdentityTestCase(test_v3.RestfulTestCase): """ # First check the credential for this user is present r = self.credential_api.get_credential(self.credential['id']) - self.assertDictEqual(r, self.credential) + self.assertDictEqual(self.credential, r) # Create a second credential with a different user - self.user2 = self.new_user_ref( - domain_id=self.domain['id'], - project_id=self.project['id']) - self.user2 = self.identity_api.create_user(self.user2) - self.credential2 = self.new_credential_ref( - user_id=self.user2['id'], - project_id=self.project['id']) - self.credential_api.create_credential( - self.credential2['id'], - self.credential2) + + user2 = unit.new_user_ref(domain_id=self.domain['id'], + project_id=self.project['id']) + user2 = self.identity_api.create_user(user2) + credential2 = unit.new_credential_ref(user_id=user2['id'], + project_id=self.project['id']) + self.credential_api.create_credential(credential2['id'], credential2) + # Create a token for this user which we can check later # gets deleted auth_data = self.build_authentication_request( @@ -371,7 +464,7 @@ class IdentityTestCase(test_v3.RestfulTestCase): # Confirm token is valid for now self.head('/auth/tokens', headers={'X-Subject-Token': token}, - expected_status=200) + expected_status=http_client.OK) # Now delete the user self.delete('/users/%(user_id)s' % { @@ -387,14 +480,57 @@ class IdentityTestCase(test_v3.RestfulTestCase): self.user['id']) self.assertEqual(0, len(tokens)) # But the credential for user2 is unaffected - r = self.credential_api.get_credential(self.credential2['id']) - self.assertDictEqual(r, self.credential2) + r = self.credential_api.get_credential(credential2['id']) + self.assertDictEqual(credential2, r) + + # shadow user tests + def test_shadow_federated_user(self): + fed_user = unit.new_federated_user_ref() + user = ( + self.identity_api.shadow_federated_user(fed_user["idp_id"], + fed_user["protocol_id"], + fed_user["unique_id"], + fed_user["display_name"]) + ) + self.assertIsNotNone(user["id"]) + self.assertEqual(len(user.keys()), 4) + self.assertIsNotNone(user['id']) + self.assertIsNotNone(user['name']) + self.assertIsNone(user['domain_id']) + self.assertEqual(user['enabled'], True) + + def test_shadow_existing_federated_user(self): + fed_user = unit.new_federated_user_ref() + + # introduce the user to keystone for the first time + shadow_user1 = self.identity_api.shadow_federated_user( + fed_user["idp_id"], + fed_user["protocol_id"], + fed_user["unique_id"], + fed_user["display_name"]) + self.assertEqual(fed_user['display_name'], shadow_user1['name']) + + # shadow the user again, with another name to invalidate the cache + # internally, this operation causes request to the driver. It should + # not fail. + fed_user['display_name'] = uuid.uuid4().hex + shadow_user2 = self.identity_api.shadow_federated_user( + fed_user["idp_id"], + fed_user["protocol_id"], + fed_user["unique_id"], + fed_user["display_name"]) + self.assertEqual(fed_user['display_name'], shadow_user2['name']) + self.assertNotEqual(shadow_user1['name'], shadow_user2['name']) + + # The shadowed users still share the same unique ID. + self.assertEqual(shadow_user1['id'], shadow_user2['id']) # group crud tests def test_create_group(self): """Call ``POST /groups``.""" - ref = self.new_group_ref(domain_id=self.domain_id) + # Create a new group to avoid a duplicate check failure + ref = unit.new_group_ref(domain_id=self.domain_id) r = self.post( '/groups', body={'group': ref}) @@ -420,7 +556,7 @@ class IdentityTestCase(test_v3.RestfulTestCase): def test_update_group(self): """Call ``PATCH /groups/{group_id}``.""" - group = self.new_group_ref(domain_id=self.domain_id) + group = unit.new_group_ref(domain_id=self.domain_id) del group['id'] r = self.patch('/groups/%(group_id)s' % { 'group_id': self.group_id}, @@ -429,19 +565,17 @@ class IdentityTestCase(test_v3.RestfulTestCase): def test_update_group_domain_id(self): """Call ``PATCH /groups/{group_id}`` with domain_id.""" - group = self.new_group_ref(domain_id=self.domain['id']) - group = self.identity_api.create_group(group) - group['domain_id'] = CONF.identity.default_domain_id + self.group['domain_id'] = CONF.identity.default_domain_id r = self.patch('/groups/%(group_id)s' % { - 'group_id': group['id']}, - body={'group': group}, + 'group_id': self.group['id']}, + body={'group': self.group}, expected_status=exception.ValidationError.code) self.config_fixture.config(domain_id_immutable=False) - group['domain_id'] = self.domain['id'] + self.group['domain_id'] = self.domain['id'] r = self.patch('/groups/%(group_id)s' % { - 'group_id': group['id']}, - body={'group': group}) - self.assertValidGroupResponse(r, group) + 'group_id': self.group['id']}, + body={'group': self.group}) + self.assertValidGroupResponse(r, self.group) def test_delete_group(self): """Call ``DELETE /groups/{group_id}``.""" @@ -453,7 +587,7 @@ class IdentityTestCase(test_v3.RestfulTestCase): log_fix = self.useFixture(fixtures.FakeLogger(level=logging.DEBUG)) - ref = self.new_user_ref(domain_id=self.domain_id) + ref = unit.new_user_ref(domain_id=self.domain_id) self.post( '/users', body={'user': ref}) @@ -467,108 +601,122 @@ class IdentityTestCase(test_v3.RestfulTestCase): log_fix = self.useFixture(fixtures.FakeLogger(level=logging.DEBUG)) # bootstrap a user as admin - user_ref = self.new_user_ref(domain_id=self.domain['id']) - password = user_ref['password'] - user_ref = self.identity_api.create_user(user_ref) + user_ref = unit.create_user(self.identity_api, + domain_id=self.domain['id']) + + self.assertNotIn(user_ref['password'], log_fix.output) # administrative password reset new_password = uuid.uuid4().hex self.patch('/users/%s' % user_ref['id'], - body={'user': {'password': new_password}}, - expected_status=200) + body={'user': {'password': new_password}}) - self.assertNotIn(password, log_fix.output) self.assertNotIn(new_password, log_fix.output) class IdentityV3toV2MethodsTestCase(unit.TestCase): """Test users V3 to V2 conversion methods.""" + def new_user_ref(self, **kwargs): + """Construct a bare bones user ref. + + Omits all optional components. + """ + ref = unit.new_user_ref(**kwargs) + # description is already omitted + del ref['email'] + del ref['enabled'] + del ref['password'] + return ref + def setUp(self): super(IdentityV3toV2MethodsTestCase, self).setUp() self.load_backends() - self.user_id = uuid.uuid4().hex - self.default_project_id = uuid.uuid4().hex - self.tenant_id = uuid.uuid4().hex + user_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + # User with only default_project_id in ref - self.user1 = {'id': self.user_id, - 'name': self.user_id, - 'default_project_id': self.default_project_id, - 'domain_id': CONF.identity.default_domain_id} + self.user1 = self.new_user_ref( + id=user_id, + name=user_id, + project_id=project_id, + domain_id=CONF.identity.default_domain_id) # User without default_project_id or tenantId in ref - self.user2 = {'id': self.user_id, - 'name': self.user_id, - 'domain_id': CONF.identity.default_domain_id} + self.user2 = self.new_user_ref( + id=user_id, + name=user_id, + domain_id=CONF.identity.default_domain_id) # User with both tenantId and default_project_id in ref - self.user3 = {'id': self.user_id, - 'name': self.user_id, - 'default_project_id': self.default_project_id, - 'tenantId': self.tenant_id, - 'domain_id': CONF.identity.default_domain_id} + self.user3 = self.new_user_ref( + id=user_id, + name=user_id, + project_id=project_id, + tenantId=project_id, + domain_id=CONF.identity.default_domain_id) # User with only tenantId in ref - self.user4 = {'id': self.user_id, - 'name': self.user_id, - 'tenantId': self.tenant_id, - 'domain_id': CONF.identity.default_domain_id} + self.user4 = self.new_user_ref( + id=user_id, + name=user_id, + tenantId=project_id, + domain_id=CONF.identity.default_domain_id) # Expected result if the user is meant to have a tenantId element - self.expected_user = {'id': self.user_id, - 'name': self.user_id, - 'username': self.user_id, - 'tenantId': self.default_project_id} + self.expected_user = {'id': user_id, + 'name': user_id, + 'username': user_id, + 'tenantId': project_id} # Expected result if the user is not meant to have a tenantId element - self.expected_user_no_tenant_id = {'id': self.user_id, - 'name': self.user_id, - 'username': self.user_id} + self.expected_user_no_tenant_id = {'id': user_id, + 'name': user_id, + 'username': user_id} def test_v3_to_v2_user_method(self): updated_user1 = controller.V2Controller.v3_to_v2_user(self.user1) self.assertIs(self.user1, updated_user1) - self.assertDictEqual(self.user1, self.expected_user) + self.assertDictEqual(self.expected_user, self.user1) updated_user2 = controller.V2Controller.v3_to_v2_user(self.user2) self.assertIs(self.user2, updated_user2) - self.assertDictEqual(self.user2, self.expected_user_no_tenant_id) + self.assertDictEqual(self.expected_user_no_tenant_id, self.user2) updated_user3 = controller.V2Controller.v3_to_v2_user(self.user3) self.assertIs(self.user3, updated_user3) - self.assertDictEqual(self.user3, self.expected_user) + self.assertDictEqual(self.expected_user, self.user3) updated_user4 = controller.V2Controller.v3_to_v2_user(self.user4) self.assertIs(self.user4, updated_user4) - self.assertDictEqual(self.user4, self.expected_user_no_tenant_id) + self.assertDictEqual(self.expected_user_no_tenant_id, self.user4) def test_v3_to_v2_user_method_list(self): user_list = [self.user1, self.user2, self.user3, self.user4] updated_list = controller.V2Controller.v3_to_v2_user(user_list) - self.assertEqual(len(updated_list), len(user_list)) + self.assertEqual(len(user_list), len(updated_list)) for i, ref in enumerate(updated_list): # Order should not change. self.assertIs(ref, user_list[i]) - self.assertDictEqual(self.user1, self.expected_user) - self.assertDictEqual(self.user2, self.expected_user_no_tenant_id) - self.assertDictEqual(self.user3, self.expected_user) - self.assertDictEqual(self.user4, self.expected_user_no_tenant_id) + self.assertDictEqual(self.expected_user, self.user1) + self.assertDictEqual(self.expected_user_no_tenant_id, self.user2) + self.assertDictEqual(self.expected_user, self.user3) + self.assertDictEqual(self.expected_user_no_tenant_id, self.user4) class UserSelfServiceChangingPasswordsTestCase(test_v3.RestfulTestCase): def setUp(self): super(UserSelfServiceChangingPasswordsTestCase, self).setUp() - self.user_ref = self.new_user_ref(domain_id=self.domain['id']) - password = self.user_ref['password'] - self.user_ref = self.identity_api.create_user(self.user_ref) - self.user_ref['password'] = password - self.token = self.get_request_token(self.user_ref['password'], 201) + self.user_ref = unit.create_user(self.identity_api, + domain_id=self.domain['id']) + self.token = self.get_request_token(self.user_ref['password'], + http_client.CREATED) def get_request_token(self, password, expected_status): auth_data = self.build_authentication_request( user_id=self.user_ref['id'], password=password) - r = self.v3_authenticate_token(auth_data, - expected_status=expected_status) + r = self.v3_create_token(auth_data, + expected_status=expected_status) return r.headers.get('X-Subject-Token') def change_password(self, expected_status, **kwargs): @@ -581,27 +729,28 @@ class UserSelfServiceChangingPasswordsTestCase(test_v3.RestfulTestCase): def test_changing_password(self): # original password works token_id = self.get_request_token(self.user_ref['password'], - expected_status=201) + expected_status=http_client.CREATED) # original token works old_token_auth = self.build_authentication_request(token=token_id) - self.v3_authenticate_token(old_token_auth, expected_status=201) + self.v3_create_token(old_token_auth) # change password new_password = uuid.uuid4().hex self.change_password(password=new_password, original_password=self.user_ref['password'], - expected_status=204) + expected_status=http_client.NO_CONTENT) # old password fails self.get_request_token(self.user_ref['password'], expected_status=http_client.UNAUTHORIZED) # old token fails - self.v3_authenticate_token(old_token_auth, - expected_status=http_client.NOT_FOUND) + self.v3_create_token(old_token_auth, + expected_status=http_client.NOT_FOUND) # new password works - self.get_request_token(new_password, expected_status=201) + self.get_request_token(new_password, + expected_status=http_client.CREATED) def test_changing_password_with_missing_original_password_fails(self): r = self.change_password(password=uuid.uuid4().hex, @@ -640,7 +789,7 @@ class UserSelfServiceChangingPasswordsTestCase(test_v3.RestfulTestCase): new_password = uuid.uuid4().hex self.change_password(password=new_password, original_password=self.user_ref['password'], - expected_status=204) + expected_status=http_client.NO_CONTENT) self.assertNotIn(self.user_ref['password'], log_fix.output) self.assertNotIn(new_password, log_fix.output) diff --git a/keystone-moon/keystone/tests/unit/test_v3_oauth1.py b/keystone-moon/keystone/tests/unit/test_v3_oauth1.py index 8794a426..198dffb8 100644 --- a/keystone-moon/keystone/tests/unit/test_v3_oauth1.py +++ b/keystone-moon/keystone/tests/unit/test_v3_oauth1.py @@ -15,28 +15,36 @@ import copy import uuid -from oslo_config import cfg +import mock +from oslo_log import versionutils from oslo_serialization import jsonutils from pycadf import cadftaxonomy from six.moves import http_client from six.moves import urllib -from keystone.contrib import oauth1 -from keystone.contrib.oauth1 import controllers -from keystone.contrib.oauth1 import core +from keystone.contrib.oauth1 import routers from keystone import exception +from keystone import oauth1 +from keystone.oauth1 import controllers +from keystone.oauth1 import core +from keystone.tests import unit from keystone.tests.unit.common import test_notifications +from keystone.tests.unit import ksfixtures from keystone.tests.unit.ksfixtures import temporaryfile from keystone.tests.unit import test_v3 -CONF = cfg.CONF +class OAuth1ContribTests(test_v3.RestfulTestCase): + @mock.patch.object(versionutils, 'report_deprecated_feature') + def test_exception_happens(self, mock_deprecator): + routers.OAuth1Extension(mock.ANY) + mock_deprecator.assert_called_once_with(mock.ANY, mock.ANY) + args, _kwargs = mock_deprecator.call_args + self.assertIn("Remove oauth1_extension from", args[1]) -class OAuth1Tests(test_v3.RestfulTestCase): - EXTENSION_NAME = 'oauth1' - EXTENSION_TO_ADD = 'oauth1_extension' +class OAuth1Tests(test_v3.RestfulTestCase): CONSUMER_URL = '/OS-OAUTH1/consumers' @@ -140,7 +148,7 @@ class ConsumerCRUDTests(OAuth1Tests): consumer = self._create_single_consumer() consumer_id = consumer['id'] resp = self.delete(self.CONSUMER_URL + '/%s' % consumer_id) - self.assertResponseStatus(resp, 204) + self.assertResponseStatus(resp, http_client.NO_CONTENT) def test_consumer_get(self): consumer = self._create_single_consumer() @@ -262,7 +270,7 @@ class OAuthFlowTests(OAuth1Tests): url = self._authorize_request_token(request_key) body = {'roles': [{'id': self.role_id}]} - resp = self.put(url, body=body, expected_status=200) + resp = self.put(url, body=body, expected_status=http_client.OK) self.verifier = resp.result['token']['oauth_verifier'] self.assertTrue(all(i in core.VERIFIER_CHARS for i in self.verifier)) self.assertEqual(8, len(self.verifier)) @@ -357,7 +365,7 @@ class AccessTokenCRUDTests(OAuthFlowTests): resp = self.delete('/users/%(user)s/OS-OAUTH1/access_tokens/%(auth)s' % {'user': self.user_id, 'auth': self.access_token.key}) - self.assertResponseStatus(resp, 204) + self.assertResponseStatus(resp, http_client.NO_CONTENT) # List access_token should be 0 resp = self.get('/users/%(user_id)s/OS-OAUTH1/access_tokens' @@ -388,7 +396,7 @@ class AuthTokenTests(OAuthFlowTests): self.assertEqual(self.role_id, roles_list[0]['id']) # verify that the token can perform delegated tasks - ref = self.new_user_ref(domain_id=self.domain_id) + ref = unit.new_user_ref(domain_id=self.domain_id) r = self.admin_request(path='/v3/users', headers=headers, method='POST', body={'user': ref}) self.assertValidUserResponse(r, ref) @@ -400,7 +408,7 @@ class AuthTokenTests(OAuthFlowTests): resp = self.delete('/users/%(user)s/OS-OAUTH1/access_tokens/%(auth)s' % {'user': self.user_id, 'auth': self.access_token.key}) - self.assertResponseStatus(resp, 204) + self.assertResponseStatus(resp, http_client.NO_CONTENT) # Check Keystone Token no longer exists headers = {'X-Subject-Token': self.keystone_token_id, @@ -415,7 +423,7 @@ class AuthTokenTests(OAuthFlowTests): consumer_id = self.consumer['key'] resp = self.delete('/OS-OAUTH1/consumers/%(consumer_id)s' % {'consumer_id': consumer_id}) - self.assertResponseStatus(resp, 204) + self.assertResponseStatus(resp, http_client.NO_CONTENT) # List access_token should be 0 resp = self.get('/users/%(user_id)s/OS-OAUTH1/access_tokens' @@ -491,7 +499,7 @@ class AuthTokenTests(OAuthFlowTests): self.keystone_token_id) def _create_trust_get_token(self): - ref = self.new_trust_ref( + ref = unit.new_trust_ref( trustor_user_id=self.user_id, trustee_user_id=self.user_id, project_id=self.project_id, @@ -534,7 +542,7 @@ class AuthTokenTests(OAuthFlowTests): def test_oauth_token_cannot_create_new_trust(self): self.test_oauth_flow() - ref = self.new_trust_ref( + ref = unit.new_trust_ref( trustor_user_id=self.user_id, trustee_user_id=self.user_id, project_id=self.project_id, @@ -588,6 +596,18 @@ class AuthTokenTests(OAuthFlowTests): expected_status=http_client.FORBIDDEN) +class FernetAuthTokenTests(AuthTokenTests): + + def config_overrides(self): + super(FernetAuthTokenTests, self).config_overrides() + self.config_fixture.config(group='token', provider='fernet') + self.useFixture(ksfixtures.KeyRepository(self.config_fixture)) + + def test_delete_keystone_tokens_by_consumer_id(self): + # NOTE(lbragstad): Fernet tokens are never persisted in the backend. + pass + + class MaliciousOAuth1Tests(OAuth1Tests): def test_bad_consumer_secret(self): @@ -645,7 +665,7 @@ class MaliciousOAuth1Tests(OAuth1Tests): url = self._authorize_request_token(request_key) body = {'roles': [{'id': self.role_id}]} - resp = self.put(url, body=body, expected_status=200) + resp = self.put(url, body=body, expected_status=http_client.OK) verifier = resp.result['token']['oauth_verifier'] self.assertIsNotNone(verifier) @@ -719,7 +739,7 @@ class MaliciousOAuth1Tests(OAuth1Tests): url = self._authorize_request_token(request_key) body = {'roles': [{'id': self.role_id}]} - resp = self.put(url, body=body, expected_status=200) + resp = self.put(url, body=body, expected_status=http_client.OK) self.verifier = resp.result['token']['oauth_verifier'] self.request_token.set_verifier(self.verifier) @@ -753,7 +773,8 @@ class MaliciousOAuth1Tests(OAuth1Tests): # NOTE(stevemar): To simulate this error, we remove the Authorization # header from the post request. del headers['Authorization'] - self.post(endpoint, headers=headers, expected_status=500) + self.post(endpoint, headers=headers, + expected_status=http_client.INTERNAL_SERVER_ERROR) class OAuthNotificationTests(OAuth1Tests, @@ -800,7 +821,6 @@ class OAuthNotificationTests(OAuth1Tests, notifications for request token creation, and access token creation/deletion are emitted. """ - consumer = self._create_single_consumer() consumer_id = consumer['id'] consumer_secret = consumer['secret'] @@ -829,7 +849,7 @@ class OAuthNotificationTests(OAuth1Tests, url = self._authorize_request_token(request_key) body = {'roles': [{'id': self.role_id}]} - resp = self.put(url, body=body, expected_status=200) + resp = self.put(url, body=body, expected_status=http_client.OK) self.verifier = resp.result['token']['oauth_verifier'] self.assertTrue(all(i in core.VERIFIER_CHARS for i in self.verifier)) self.assertEqual(8, len(self.verifier)) @@ -858,7 +878,7 @@ class OAuthNotificationTests(OAuth1Tests, resp = self.delete('/users/%(user)s/OS-OAUTH1/access_tokens/%(auth)s' % {'user': self.user_id, 'auth': self.access_token.key}) - self.assertResponseStatus(resp, 204) + self.assertResponseStatus(resp, http_client.NO_CONTENT) # Test to ensure the delete access token notification is sent self._assert_notify_sent(access_key, @@ -873,7 +893,7 @@ class OAuthNotificationTests(OAuth1Tests, class OAuthCADFNotificationTests(OAuthNotificationTests): def setUp(self): - """Repeat the tests for CADF notifications """ + """Repeat the tests for CADF notifications.""" super(OAuthCADFNotificationTests, self).setUp() self.config_fixture.config(notification_format='cadf') diff --git a/keystone-moon/keystone/tests/unit/test_v3_os_revoke.py b/keystone-moon/keystone/tests/unit/test_v3_os_revoke.py index 86ced724..5fb5387a 100644 --- a/keystone-moon/keystone/tests/unit/test_v3_os_revoke.py +++ b/keystone-moon/keystone/tests/unit/test_v3_os_revoke.py @@ -19,7 +19,7 @@ from six.moves import http_client from testtools import matchers from keystone.common import utils -from keystone.contrib.revoke import model +from keystone.models import revoke_model from keystone.tests.unit import test_v3 from keystone.token import provider @@ -31,8 +31,6 @@ def _future_time_string(): class OSRevokeTests(test_v3.RestfulTestCase, test_v3.JsonHomeTestMixin): - EXTENSION_NAME = 'revoke' - EXTENSION_TO_ADD = 'revoke_extension' JSON_HOME_DATA = { 'http://docs.openstack.org/api/openstack-identity/3/ext/OS-REVOKE/1.0/' @@ -92,7 +90,7 @@ class OSRevokeTests(test_v3.RestfulTestCase, test_v3.JsonHomeTestMixin): sample['project_id'] = six.text_type(project_id) before_time = timeutils.utcnow() self.revoke_api.revoke( - model.RevokeEvent(project_id=project_id)) + revoke_model.RevokeEvent(project_id=project_id)) resp = self.get('/OS-REVOKE/events') events = resp.json_body['events'] @@ -105,7 +103,7 @@ class OSRevokeTests(test_v3.RestfulTestCase, test_v3.JsonHomeTestMixin): sample['domain_id'] = six.text_type(domain_id) before_time = timeutils.utcnow() self.revoke_api.revoke( - model.RevokeEvent(domain_id=domain_id)) + revoke_model.RevokeEvent(domain_id=domain_id)) resp = self.get('/OS-REVOKE/events') events = resp.json_body['events'] @@ -127,7 +125,7 @@ class OSRevokeTests(test_v3.RestfulTestCase, test_v3.JsonHomeTestMixin): sample['domain_id'] = six.text_type(domain_id) self.revoke_api.revoke( - model.RevokeEvent(domain_id=domain_id)) + revoke_model.RevokeEvent(domain_id=domain_id)) resp = self.get('/OS-REVOKE/events') events = resp.json_body['events'] diff --git a/keystone-moon/keystone/tests/unit/test_v3_policy.py b/keystone-moon/keystone/tests/unit/test_v3_policy.py index 538fc565..76a52088 100644 --- a/keystone-moon/keystone/tests/unit/test_v3_policy.py +++ b/keystone-moon/keystone/tests/unit/test_v3_policy.py @@ -12,8 +12,10 @@ # License for the specific language governing permissions and limitations # under the License. +import json import uuid +from keystone.tests import unit from keystone.tests.unit import test_v3 @@ -22,9 +24,8 @@ class PolicyTestCase(test_v3.RestfulTestCase): def setUp(self): super(PolicyTestCase, self).setUp() - self.policy_id = uuid.uuid4().hex - self.policy = self.new_policy_ref() - self.policy['id'] = self.policy_id + self.policy = unit.new_policy_ref() + self.policy_id = self.policy['id'] self.policy_api.create_policy( self.policy_id, self.policy.copy()) @@ -33,10 +34,8 @@ class PolicyTestCase(test_v3.RestfulTestCase): def test_create_policy(self): """Call ``POST /policies``.""" - ref = self.new_policy_ref() - r = self.post( - '/policies', - body={'policy': ref}) + ref = unit.new_policy_ref() + r = self.post('/policies', body={'policy': ref}) return self.assertValidPolicyResponse(r, ref) def test_list_policies(self): @@ -47,22 +46,18 @@ class PolicyTestCase(test_v3.RestfulTestCase): def test_get_policy(self): """Call ``GET /policies/{policy_id}``.""" r = self.get( - '/policies/%(policy_id)s' % { - 'policy_id': self.policy_id}) + '/policies/%(policy_id)s' % {'policy_id': self.policy_id}) self.assertValidPolicyResponse(r, self.policy) def test_update_policy(self): """Call ``PATCH /policies/{policy_id}``.""" - policy = self.new_policy_ref() - policy['id'] = self.policy_id + self.policy['blob'] = json.dumps({'data': uuid.uuid4().hex, }) r = self.patch( - '/policies/%(policy_id)s' % { - 'policy_id': self.policy_id}, - body={'policy': policy}) - self.assertValidPolicyResponse(r, policy) + '/policies/%(policy_id)s' % {'policy_id': self.policy_id}, + body={'policy': self.policy}) + self.assertValidPolicyResponse(r, self.policy) def test_delete_policy(self): """Call ``DELETE /policies/{policy_id}``.""" self.delete( - '/policies/%(policy_id)s' % { - 'policy_id': self.policy_id}) + '/policies/%(policy_id)s' % {'policy_id': self.policy_id}) diff --git a/keystone-moon/keystone/tests/unit/test_v3_protection.py b/keystone-moon/keystone/tests/unit/test_v3_protection.py index 9922ae5e..f77a1528 100644 --- a/keystone-moon/keystone/tests/unit/test_v3_protection.py +++ b/keystone-moon/keystone/tests/unit/test_v3_protection.py @@ -20,19 +20,22 @@ from oslo_serialization import jsonutils from six.moves import http_client from keystone import exception -from keystone.policy.backends import rules from keystone.tests import unit +from keystone.tests.unit import ksfixtures from keystone.tests.unit.ksfixtures import temporaryfile from keystone.tests.unit import test_v3 +from keystone.tests.unit import utils CONF = cfg.CONF -DEFAULT_DOMAIN_ID = CONF.identity.default_domain_id class IdentityTestProtectedCase(test_v3.RestfulTestCase): """Test policy enforcement on the v3 Identity API.""" + def _policy_fixture(self): + return ksfixtures.Policy(self.tmpfilename, self.config_fixture) + def setUp(self): """Setup for Identity Protection Test Cases. @@ -49,14 +52,9 @@ class IdentityTestProtectedCase(test_v3.RestfulTestCase): the default domain. """ - # Ensure that test_v3.RestfulTestCase doesn't load its own - # sample data, which would make checking the results of our - # tests harder - super(IdentityTestProtectedCase, self).setUp() self.tempfile = self.useFixture(temporaryfile.SecureTempFile()) self.tmpfilename = self.tempfile.file_name - self.config_fixture.config(group='oslo_policy', - policy_file=self.tmpfilename) + super(IdentityTestProtectedCase, self).setUp() # A default auth request we can use - un-scoped user token self.auth = self.build_authentication_request( @@ -66,45 +64,33 @@ class IdentityTestProtectedCase(test_v3.RestfulTestCase): def load_sample_data(self): self._populate_default_domain() # Start by creating a couple of domains - self.domainA = self.new_domain_ref() + self.domainA = unit.new_domain_ref() self.resource_api.create_domain(self.domainA['id'], self.domainA) - self.domainB = self.new_domain_ref() + self.domainB = unit.new_domain_ref() self.resource_api.create_domain(self.domainB['id'], self.domainB) - self.domainC = self.new_domain_ref() - self.domainC['enabled'] = False + self.domainC = unit.new_domain_ref(enabled=False) self.resource_api.create_domain(self.domainC['id'], self.domainC) # Now create some users, one in domainA and two of them in domainB - self.user1 = self.new_user_ref(domain_id=self.domainA['id']) - password = uuid.uuid4().hex - self.user1['password'] = password - self.user1 = self.identity_api.create_user(self.user1) - self.user1['password'] = password - - self.user2 = self.new_user_ref(domain_id=self.domainB['id']) - password = uuid.uuid4().hex - self.user2['password'] = password - self.user2 = self.identity_api.create_user(self.user2) - self.user2['password'] = password - - self.user3 = self.new_user_ref(domain_id=self.domainB['id']) - password = uuid.uuid4().hex - self.user3['password'] = password - self.user3 = self.identity_api.create_user(self.user3) - self.user3['password'] = password - - self.group1 = self.new_group_ref(domain_id=self.domainA['id']) + self.user1 = unit.create_user(self.identity_api, + domain_id=self.domainA['id']) + self.user2 = unit.create_user(self.identity_api, + domain_id=self.domainB['id']) + self.user3 = unit.create_user(self.identity_api, + domain_id=self.domainB['id']) + + self.group1 = unit.new_group_ref(domain_id=self.domainA['id']) self.group1 = self.identity_api.create_group(self.group1) - self.group2 = self.new_group_ref(domain_id=self.domainA['id']) + self.group2 = unit.new_group_ref(domain_id=self.domainA['id']) self.group2 = self.identity_api.create_group(self.group2) - self.group3 = self.new_group_ref(domain_id=self.domainB['id']) + self.group3 = unit.new_group_ref(domain_id=self.domainB['id']) self.group3 = self.identity_api.create_group(self.group3) - self.role = self.new_role_ref() + self.role = unit.new_role_ref() self.role_api.create_role(self.role['id'], self.role) - self.role1 = self.new_role_ref() + self.role1 = unit.new_role_ref() self.role_api.create_role(self.role1['id'], self.role1) self.assignment_api.create_grant(self.role['id'], user_id=self.user1['id'], @@ -348,34 +334,23 @@ class IdentityTestPolicySample(test_v3.RestfulTestCase): def load_sample_data(self): self._populate_default_domain() - self.just_a_user = self.new_user_ref( + self.just_a_user = unit.create_user( + self.identity_api, domain_id=CONF.identity.default_domain_id) - password = uuid.uuid4().hex - self.just_a_user['password'] = password - self.just_a_user = self.identity_api.create_user(self.just_a_user) - self.just_a_user['password'] = password - - self.another_user = self.new_user_ref( + self.another_user = unit.create_user( + self.identity_api, domain_id=CONF.identity.default_domain_id) - password = uuid.uuid4().hex - self.another_user['password'] = password - self.another_user = self.identity_api.create_user(self.another_user) - self.another_user['password'] = password - - self.admin_user = self.new_user_ref( + self.admin_user = unit.create_user( + self.identity_api, domain_id=CONF.identity.default_domain_id) - password = uuid.uuid4().hex - self.admin_user['password'] = password - self.admin_user = self.identity_api.create_user(self.admin_user) - self.admin_user['password'] = password - self.role = self.new_role_ref() + self.role = unit.new_role_ref() self.role_api.create_role(self.role['id'], self.role) - self.admin_role = {'id': uuid.uuid4().hex, 'name': 'admin'} + self.admin_role = unit.new_role_ref(name='admin') self.role_api.create_role(self.admin_role['id'], self.admin_role) # Create and assign roles to the project - self.project = self.new_project_ref( + self.project = unit.new_project_ref( domain_id=CONF.identity.default_domain_id) self.resource_api.create_project(self.project['id'], self.project) self.assignment_api.create_grant(self.role['id'], @@ -461,7 +436,8 @@ class IdentityTestPolicySample(test_v3.RestfulTestCase): token = self.get_requested_token(auth) self.head('/auth/tokens', token=token, - headers={'X-Subject-Token': token}, expected_status=200) + headers={'X-Subject-Token': token}, + expected_status=http_client.OK) def test_user_check_user_token(self): # A user can check one of their own tokens. @@ -474,7 +450,8 @@ class IdentityTestPolicySample(test_v3.RestfulTestCase): token2 = self.get_requested_token(auth) self.head('/auth/tokens', token=token1, - headers={'X-Subject-Token': token2}, expected_status=200) + headers={'X-Subject-Token': token2}, + expected_status=http_client.OK) def test_user_check_other_user_token_rejected(self): # A user cannot check another user's token. @@ -510,7 +487,8 @@ class IdentityTestPolicySample(test_v3.RestfulTestCase): user_token = self.get_requested_token(user_auth) self.head('/auth/tokens', token=admin_token, - headers={'X-Subject-Token': user_token}, expected_status=200) + headers={'X-Subject-Token': user_token}, + expected_status=http_client.OK) def test_user_revoke_same_token(self): # Given a non-admin user token, the token can be used to revoke @@ -579,6 +557,10 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, test_v3.AssignmentTestMixin): """Test policy enforcement of the sample v3 cloud policy file.""" + def _policy_fixture(self): + return ksfixtures.Policy(unit.dirs.etc('policy.v3cloudsample.json'), + self.config_fixture) + def setUp(self): """Setup for v3 Cloud Policy Sample Test Cases. @@ -592,8 +574,8 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, - domain_admin_user has role 'admin' on domainA, - project_admin_user has role 'admin' on the project, - just_a_user has a non-admin role on both domainA and the project. - - admin_domain has user cloud_admin_user, with an 'admin' role - on admin_domain. + - admin_domain has admin_project, and user cloud_admin_user, with an + 'admin' role on admin_project. We test various api protection rules from the cloud sample policy file to make sure the sample is valid and that we correctly enforce it. @@ -604,62 +586,61 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, # tests harder super(IdentityTestv3CloudPolicySample, self).setUp() - # Finally, switch to the v3 sample policy file - self.addCleanup(rules.reset) - rules.reset() self.config_fixture.config( - group='oslo_policy', - policy_file=unit.dirs.etc('policy.v3cloudsample.json')) + group='resource', + admin_project_name=self.admin_project['name']) + self.config_fixture.config( + group='resource', + admin_project_domain_name=self.admin_domain['name']) def load_sample_data(self): # Start by creating a couple of domains self._populate_default_domain() - self.domainA = self.new_domain_ref() + self.domainA = unit.new_domain_ref() self.resource_api.create_domain(self.domainA['id'], self.domainA) - self.domainB = self.new_domain_ref() + self.domainB = unit.new_domain_ref() self.resource_api.create_domain(self.domainB['id'], self.domainB) - self.admin_domain = {'id': 'admin_domain_id', 'name': 'Admin_domain'} + self.admin_domain = unit.new_domain_ref() self.resource_api.create_domain(self.admin_domain['id'], self.admin_domain) + self.admin_project = unit.new_project_ref( + domain_id=self.admin_domain['id']) + self.resource_api.create_project(self.admin_project['id'], + self.admin_project) + # And our users - self.cloud_admin_user = self.new_user_ref( + self.cloud_admin_user = unit.create_user( + self.identity_api, domain_id=self.admin_domain['id']) - password = uuid.uuid4().hex - self.cloud_admin_user['password'] = password - self.cloud_admin_user = ( - self.identity_api.create_user(self.cloud_admin_user)) - self.cloud_admin_user['password'] = password - self.just_a_user = self.new_user_ref(domain_id=self.domainA['id']) - password = uuid.uuid4().hex - self.just_a_user['password'] = password - self.just_a_user = self.identity_api.create_user(self.just_a_user) - self.just_a_user['password'] = password - self.domain_admin_user = self.new_user_ref( + self.just_a_user = unit.create_user( + self.identity_api, domain_id=self.domainA['id']) - password = uuid.uuid4().hex - self.domain_admin_user['password'] = password - self.domain_admin_user = ( - self.identity_api.create_user(self.domain_admin_user)) - self.domain_admin_user['password'] = password - self.project_admin_user = self.new_user_ref( + self.domain_admin_user = unit.create_user( + self.identity_api, + domain_id=self.domainA['id']) + self.domainB_admin_user = unit.create_user( + self.identity_api, + domain_id=self.domainB['id']) + self.project_admin_user = unit.create_user( + self.identity_api, domain_id=self.domainA['id']) - password = uuid.uuid4().hex - self.project_admin_user['password'] = password - self.project_admin_user = ( - self.identity_api.create_user(self.project_admin_user)) - self.project_admin_user['password'] = password - - # The admin role and another plain role - self.admin_role = {'id': uuid.uuid4().hex, 'name': 'admin'} + self.project_adminB_user = unit.create_user( + self.identity_api, + domain_id=self.domainB['id']) + + # The admin role, a domain specific role and another plain role + self.admin_role = unit.new_role_ref(name='admin') self.role_api.create_role(self.admin_role['id'], self.admin_role) - self.role = self.new_role_ref() + self.roleA = unit.new_role_ref(domain_id=self.domainA['id']) + self.role_api.create_role(self.roleA['id'], self.roleA) + self.role = unit.new_role_ref() self.role_api.create_role(self.role['id'], self.role) - # The cloud admin just gets the admin role + # The cloud admin just gets the admin role on the special admin project self.assignment_api.create_grant(self.admin_role['id'], user_id=self.cloud_admin_user['id'], - domain_id=self.admin_domain['id']) + project_id=self.admin_project['id']) # Assign roles to the domain self.assignment_api.create_grant(self.admin_role['id'], @@ -668,13 +649,21 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, self.assignment_api.create_grant(self.role['id'], user_id=self.just_a_user['id'], domain_id=self.domainA['id']) + self.assignment_api.create_grant(self.admin_role['id'], + user_id=self.domainB_admin_user['id'], + domain_id=self.domainB['id']) # Create and assign roles to the project - self.project = self.new_project_ref(domain_id=self.domainA['id']) + self.project = unit.new_project_ref(domain_id=self.domainA['id']) self.resource_api.create_project(self.project['id'], self.project) + self.projectB = unit.new_project_ref(domain_id=self.domainB['id']) + self.resource_api.create_project(self.projectB['id'], self.projectB) self.assignment_api.create_grant(self.admin_role['id'], user_id=self.project_admin_user['id'], project_id=self.project['id']) + self.assignment_api.create_grant( + self.admin_role['id'], user_id=self.project_adminB_user['id'], + project_id=self.projectB['id']) self.assignment_api.create_grant(self.role['id'], user_id=self.just_a_user['id'], project_id=self.project['id']) @@ -683,7 +672,8 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, # Return the expected return codes for APIs with and without data # with any specified status overriding the normal values if expected_status is None: - return (200, 201, 204) + return (http_client.OK, http_client.CREATED, + http_client.NO_CONTENT) else: return (expected_status, expected_status, expected_status) @@ -702,7 +692,7 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, self.delete(entity_url, auth=self.auth, expected_status=status_no_data) - user_ref = self.new_user_ref(domain_id=domain_id) + user_ref = unit.new_user_ref(domain_id=domain_id) self.post('/users', auth=self.auth, body={'user': user_ref}, expected_status=status_created) @@ -721,7 +711,7 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, self.delete(entity_url, auth=self.auth, expected_status=status_no_data) - proj_ref = self.new_project_ref(domain_id=domain_id) + proj_ref = unit.new_project_ref(domain_id=domain_id) self.post('/projects', auth=self.auth, body={'project': proj_ref}, expected_status=status_created) @@ -740,13 +730,14 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, self.delete(entity_url, auth=self.auth, expected_status=status_no_data) - domain_ref = self.new_domain_ref() + domain_ref = unit.new_domain_ref() self.post('/domains', auth=self.auth, body={'domain': domain_ref}, expected_status=status_created) - def _test_grants(self, target, entity_id, expected=None): + def _test_grants(self, target, entity_id, role_domain_id=None, + list_status_OK=False, expected=None): status_OK, status_created, status_no_data = self._stati(expected) - a_role = {'id': uuid.uuid4().hex, 'name': uuid.uuid4().hex} + a_role = unit.new_role_ref(domain_id=role_domain_id) self.role_api.create_role(a_role['id'], a_role) collection_url = ( @@ -762,11 +753,67 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, expected_status=status_no_data) self.head(member_url, auth=self.auth, expected_status=status_no_data) - self.get(collection_url, auth=self.auth, - expected_status=status_OK) + if list_status_OK: + self.get(collection_url, auth=self.auth) + else: + self.get(collection_url, auth=self.auth, + expected_status=status_OK) self.delete(member_url, auth=self.auth, expected_status=status_no_data) + def _role_management_cases(self, read_status_OK=False, expected=None): + # Set the different status values for different types of call depending + # on whether we expect the calls to fail or not. + status_OK, status_created, status_no_data = self._stati(expected) + entity_url = '/roles/%s' % self.role['id'] + list_url = '/roles' + + if read_status_OK: + self.get(entity_url, auth=self.auth) + self.get(list_url, auth=self.auth) + else: + self.get(entity_url, auth=self.auth, + expected_status=status_OK) + self.get(list_url, auth=self.auth, + expected_status=status_OK) + + role = {'name': 'Updated'} + self.patch(entity_url, auth=self.auth, body={'role': role}, + expected_status=status_OK) + self.delete(entity_url, auth=self.auth, + expected_status=status_no_data) + + role_ref = unit.new_role_ref() + self.post('/roles', auth=self.auth, body={'role': role_ref}, + expected_status=status_created) + + def _domain_role_management_cases(self, domain_id, read_status_OK=False, + expected=None): + # Set the different status values for different types of call depending + # on whether we expect the calls to fail or not. + status_OK, status_created, status_no_data = self._stati(expected) + entity_url = '/roles/%s' % self.roleA['id'] + list_url = '/roles?domain_id=%s' % domain_id + + if read_status_OK: + self.get(entity_url, auth=self.auth) + self.get(list_url, auth=self.auth) + else: + self.get(entity_url, auth=self.auth, + expected_status=status_OK) + self.get(list_url, auth=self.auth, + expected_status=status_OK) + + role = {'name': 'Updated'} + self.patch(entity_url, auth=self.auth, body={'role': role}, + expected_status=status_OK) + self.delete(entity_url, auth=self.auth, + expected_status=status_no_data) + + role_ref = unit.new_role_ref(domain_id=domain_id) + self.post('/roles', auth=self.auth, body={'role': role_ref}, + expected_status=status_created) + def test_user_management(self): # First, authenticate with a user that does not have the domain # admin role - shouldn't be able to do much. @@ -786,13 +833,90 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, self._test_user_management(self.domainA['id']) + def test_user_management_normalized_keys(self): + """Illustrate the inconsistent handling of hyphens in keys. + + To quote Morgan in bug 1526244: + + the reason this is converted from "domain-id" to "domain_id" is + because of how we process/normalize data. The way we have to handle + specific data types for known columns requires avoiding "-" in the + actual python code since "-" is not valid for attributes in python + w/o significant use of "getattr" etc. + + In short, historically we handle some things in conversions. The + use of "extras" has long been a poor design choice that leads to + odd/strange inconsistent behaviors because of other choices made in + handling data from within the body. (In many cases we convert from + "-" to "_" throughout openstack) + + Source: https://bugs.launchpad.net/keystone/+bug/1526244/comments/9 + + """ + # Authenticate with a user that has the domain admin role + self.auth = self.build_authentication_request( + user_id=self.domain_admin_user['id'], + password=self.domain_admin_user['password'], + domain_id=self.domainA['id']) + + # Show that we can read a normal user without any surprises. + r = self.get( + '/users/%s' % self.just_a_user['id'], + auth=self.auth, + expected_status=http_client.OK) + self.assertValidUserResponse(r) + + # We don't normalize query string keys, so both of these result in a + # 403, because we didn't specify a domain_id query string in either + # case, and we explicitly require one (it doesn't matter what + # 'domain-id' value you use). + self.get( + '/users?domain-id=%s' % self.domainA['id'], + auth=self.auth, + expected_status=exception.ForbiddenAction.code) + self.get( + '/users?domain-id=%s' % self.domainB['id'], + auth=self.auth, + expected_status=exception.ForbiddenAction.code) + + # If we try updating the user's 'domain_id' by specifying a + # 'domain-id', then it'll be stored into extras rather than normalized, + # and the user's actual 'domain_id' is not affected. + r = self.patch( + '/users/%s' % self.just_a_user['id'], + auth=self.auth, + body={'user': {'domain-id': self.domainB['id']}}, + expected_status=http_client.OK) + self.assertEqual(self.domainB['id'], r.json['user']['domain-id']) + self.assertEqual(self.domainA['id'], r.json['user']['domain_id']) + self.assertNotEqual(self.domainB['id'], self.just_a_user['domain_id']) + self.assertValidUserResponse(r, self.just_a_user) + + # Finally, show that we can create a new user without any surprises. + # But if we specify a 'domain-id' instead of a 'domain_id', we get a + # Forbidden response because we fail a policy check before + # normalization occurs. + user_ref = unit.new_user_ref(domain_id=self.domainA['id']) + r = self.post( + '/users', + auth=self.auth, + body={'user': user_ref}, + expected_status=http_client.CREATED) + self.assertValidUserResponse(r, ref=user_ref) + user_ref['domain-id'] = user_ref.pop('domain_id') + self.post( + '/users', + auth=self.auth, + body={'user': user_ref}, + expected_status=exception.ForbiddenAction.code) + def test_user_management_by_cloud_admin(self): # Test users management with a cloud admin. This user should # be able to manage users in any domain. self.auth = self.build_authentication_request( user_id=self.cloud_admin_user['id'], password=self.cloud_admin_user['password'], - domain_id=self.admin_domain['id']) + project_id=self.admin_project['id']) self._test_user_management(self.domainA['id']) @@ -824,7 +948,7 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, self.auth = self.build_authentication_request( user_id=self.cloud_admin_user['id'], password=self.cloud_admin_user['password'], - domain_id=self.admin_domain['id']) + project_id=self.admin_project['id']) # Check whether cloud admin can operate a domain # other than its own domain or not @@ -858,10 +982,56 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, self.auth = self.build_authentication_request( user_id=self.cloud_admin_user['id'], password=self.cloud_admin_user['password'], - domain_id=self.admin_domain['id']) + project_id=self.admin_project['id']) self._test_grants('domains', self.domainA['id']) + def test_domain_grants_by_cloud_admin_for_domain_specific_role(self): + # Test domain grants with a cloud admin. This user should be + # able to manage domain roles on any domain. + self.auth = self.build_authentication_request( + user_id=self.cloud_admin_user['id'], + password=self.cloud_admin_user['password'], + project_id=self.admin_project['id']) + + self._test_grants('domains', self.domainA['id'], + role_domain_id=self.domainB['id']) + + def test_domain_grants_by_non_admin_for_domain_specific_role(self): + # A non-admin shouldn't be able to do anything + self.auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password'], + domain_id=self.domainA['id']) + + self._test_grants('domains', self.domainA['id'], + role_domain_id=self.domainA['id'], + expected=exception.ForbiddenAction.code) + self._test_grants('domains', self.domainA['id'], + role_domain_id=self.domainB['id'], + expected=exception.ForbiddenAction.code) + + def test_domain_grants_by_domain_admin_for_domain_specific_role(self): + # Authenticate with a user that does have the domain admin role, + # should not be able to assign a domain_specific role from another + # domain + self.auth = self.build_authentication_request( + user_id=self.domain_admin_user['id'], + password=self.domain_admin_user['password'], + domain_id=self.domainA['id']) + + self._test_grants('domains', self.domainA['id'], + role_domain_id=self.domainB['id'], + # List status will always be OK, since we are not + # granting/checking/deleting assignments + list_status_OK=True, + expected=exception.ForbiddenAction.code) + + # They should be able to assign a domain specific role from the same + # domain + self._test_grants('domains', self.domainA['id'], + role_domain_id=self.domainA['id']) + def test_project_grants(self): self.auth = self.build_authentication_request( user_id=self.just_a_user['id'], @@ -890,11 +1060,67 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, self._test_grants('projects', self.project['id']) + def test_project_grants_by_non_admin_for_domain_specific_role(self): + # A non-admin shouldn't be able to do anything + self.auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password'], + project_id=self.project['id']) + + self._test_grants('projects', self.project['id'], + role_domain_id=self.domainA['id'], + expected=exception.ForbiddenAction.code) + self._test_grants('projects', self.project['id'], + role_domain_id=self.domainB['id'], + expected=exception.ForbiddenAction.code) + + def test_project_grants_by_project_admin_for_domain_specific_role(self): + # Authenticate with a user that does have the project admin role, + # should not be able to assign a domain_specific role from another + # domain + self.auth = self.build_authentication_request( + user_id=self.project_admin_user['id'], + password=self.project_admin_user['password'], + project_id=self.project['id']) + + self._test_grants('projects', self.project['id'], + role_domain_id=self.domainB['id'], + # List status will always be OK, since we are not + # granting/checking/deleting assignments + list_status_OK=True, + expected=exception.ForbiddenAction.code) + + # They should be able to assign a domain specific role from the same + # domain + self._test_grants('projects', self.project['id'], + role_domain_id=self.domainA['id']) + + def test_project_grants_by_domain_admin_for_domain_specific_role(self): + # Authenticate with a user that does have the domain admin role, + # should not be able to assign a domain_specific role from another + # domain + self.auth = self.build_authentication_request( + user_id=self.domain_admin_user['id'], + password=self.domain_admin_user['password'], + domain_id=self.domainA['id']) + + self._test_grants('projects', self.project['id'], + role_domain_id=self.domainB['id'], + # List status will always be OK, since we are not + # granting/checking/deleting assignments + list_status_OK=True, + expected=exception.ForbiddenAction.code) + + # They should be able to assign a domain specific role from the same + # domain + self._test_grants('projects', self.project['id'], + role_domain_id=self.domainA['id']) + def test_cloud_admin_list_assignments_of_domain(self): self.auth = self.build_authentication_request( user_id=self.cloud_admin_user['id'], password=self.cloud_admin_user['password'], - domain_id=self.admin_domain['id']) + project_id=self.admin_project['id']) collection_url = self.build_role_assignment_query_url( domain_id=self.domainA['id']) @@ -968,7 +1194,7 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, self.auth = self.build_authentication_request( user_id=self.cloud_admin_user['id'], password=self.cloud_admin_user['password'], - domain_id=self.admin_domain['id']) + project_id=self.admin_project['id']) collection_url = self.build_role_assignment_query_url( project_id=self.project['id']) @@ -990,7 +1216,33 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, self.assertRoleAssignmentInListResponse(r, project_admin_entity) self.assertRoleAssignmentInListResponse(r, project_user_entity) - @unit.utils.wip('waiting on bug #1437407') + def test_admin_project_list_assignments_of_project(self): + self.auth = self.build_authentication_request( + user_id=self.project_admin_user['id'], + password=self.project_admin_user['password'], + project_id=self.project['id']) + + collection_url = self.build_role_assignment_query_url( + project_id=self.project['id']) + r = self.get(collection_url, auth=self.auth) + self.assertValidRoleAssignmentListResponse( + r, expected_length=2, resource_url=collection_url) + + project_admin_entity = self.build_role_assignment_entity( + project_id=self.project['id'], + user_id=self.project_admin_user['id'], + role_id=self.admin_role['id'], + inherited_to_projects=False) + project_user_entity = self.build_role_assignment_entity( + project_id=self.project['id'], + user_id=self.just_a_user['id'], + role_id=self.role['id'], + inherited_to_projects=False) + + self.assertRoleAssignmentInListResponse(r, project_admin_entity) + self.assertRoleAssignmentInListResponse(r, project_user_entity) + + @utils.wip('waiting on bug #1437407') def test_domain_admin_list_assignments_of_project(self): self.auth = self.build_authentication_request( user_id=self.domain_admin_user['id'], @@ -1017,6 +1269,53 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, self.assertRoleAssignmentInListResponse(r, project_admin_entity) self.assertRoleAssignmentInListResponse(r, project_user_entity) + def test_domain_admin_list_assignment_tree(self): + # Add a child project to the standard test data + sub_project = unit.new_project_ref(domain_id=self.domainA['id'], + parent_id=self.project['id']) + self.resource_api.create_project(sub_project['id'], sub_project) + self.assignment_api.create_grant(self.role['id'], + user_id=self.just_a_user['id'], + project_id=sub_project['id']) + + collection_url = self.build_role_assignment_query_url( + project_id=self.project['id']) + collection_url += '&include_subtree=True' + + # The domain admin should be able to list the assignment tree + auth = self.build_authentication_request( + user_id=self.domain_admin_user['id'], + password=self.domain_admin_user['password'], + domain_id=self.domainA['id']) + + r = self.get(collection_url, auth=auth) + self.assertValidRoleAssignmentListResponse( + r, expected_length=3, resource_url=collection_url) + + # A project admin should not be able to + auth = self.build_authentication_request( + user_id=self.project_admin_user['id'], + password=self.project_admin_user['password'], + project_id=self.project['id']) + + r = self.get(collection_url, auth=auth, + expected_status=http_client.FORBIDDEN) + + # A neither should a domain admin from a different domain + domainB_admin_user = unit.create_user( + self.identity_api, + domain_id=self.domainB['id']) + self.assignment_api.create_grant(self.admin_role['id'], + user_id=domainB_admin_user['id'], + domain_id=self.domainB['id']) + auth = self.build_authentication_request( + user_id=domainB_admin_user['id'], + password=domainB_admin_user['password'], + domain_id=self.domainB['id']) + + r = self.get(collection_url, auth=auth, + expected_status=http_client.FORBIDDEN) + def test_domain_user_list_assignments_of_project_failed(self): self.auth = self.build_authentication_request( user_id=self.just_a_user['id'], @@ -1040,7 +1339,23 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, self.auth = self.build_authentication_request( user_id=self.cloud_admin_user['id'], password=self.cloud_admin_user['password'], - domain_id=self.admin_domain['id']) + project_id=self.admin_project['id']) + + self._test_domain_management() + + def test_admin_project(self): + self.auth = self.build_authentication_request( + user_id=self.project_admin_user['id'], + password=self.project_admin_user['password'], + project_id=self.project['id']) + + self._test_domain_management( + expected=exception.ForbiddenAction.code) + + self.auth = self.build_authentication_request( + user_id=self.cloud_admin_user['id'], + password=self.cloud_admin_user['password'], + project_id=self.admin_project['id']) self._test_domain_management() @@ -1050,16 +1365,15 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, password=self.domain_admin_user['password'], domain_id=self.domainA['id']) entity_url = '/domains/%s' % self.domainA['id'] - self.get(entity_url, auth=self.auth, expected_status=200) + self.get(entity_url, auth=self.auth) def test_list_user_credentials(self): - self.credential_user = self.new_credential_ref(self.just_a_user['id']) - self.credential_api.create_credential(self.credential_user['id'], - self.credential_user) - self.credential_admin = self.new_credential_ref( - self.cloud_admin_user['id']) - self.credential_api.create_credential(self.credential_admin['id'], - self.credential_admin) + credential_user = unit.new_credential_ref(self.just_a_user['id']) + self.credential_api.create_credential(credential_user['id'], + credential_user) + credential_admin = unit.new_credential_ref(self.cloud_admin_user['id']) + self.credential_api.create_credential(credential_admin['id'], + credential_admin) self.auth = self.build_authentication_request( user_id=self.just_a_user['id'], @@ -1075,9 +1389,8 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, def test_get_and_delete_ec2_credentials(self): """Tests getting and deleting ec2 credentials through the ec2 API.""" - another_user = self.new_user_ref(domain_id=self.domainA['id']) - password = another_user['password'] - another_user = self.identity_api.create_user(another_user) + another_user = unit.create_user(self.identity_api, + domain_id=self.domainA['id']) # create a credential for just_a_user just_user_auth = self.build_authentication_request( @@ -1091,7 +1404,7 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, # another normal user can't get the credential another_user_auth = self.build_authentication_request( user_id=another_user['id'], - password=password) + password=another_user['password']) another_user_url = '/users/%s/credentials/OS-EC2/%s' % ( another_user['id'], r.result['credential']['access']) self.get(another_user_url, auth=another_user_auth, @@ -1160,7 +1473,26 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, admin_auth = self.build_authentication_request( user_id=self.cloud_admin_user['id'], password=self.cloud_admin_user['password'], - domain_id=self.admin_domain['id']) + project_id=self.admin_project['id']) + admin_token = self.get_requested_token(admin_auth) + + user_auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password']) + user_token = self.get_requested_token(user_auth) + + self.get('/auth/tokens', token=admin_token, + headers={'X-Subject-Token': user_token}) + + def test_admin_project_validate_user_token(self): + # An admin can validate a user's token. + # This is GET /v3/auth/tokens + + admin_auth = self.build_authentication_request( + user_id=self.project_admin_user['id'], + password=self.project_admin_user['password'], + project_id=self.project['id']) + admin_token = self.get_requested_token(admin_auth) user_auth = self.build_authentication_request( @@ -1182,7 +1514,8 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, token = self.get_requested_token(auth) self.head('/auth/tokens', token=token, - headers={'X-Subject-Token': token}, expected_status=200) + headers={'X-Subject-Token': token}, + expected_status=http_client.OK) def test_user_check_user_token(self): # A user can check one of their own tokens. @@ -1195,7 +1528,8 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, token2 = self.get_requested_token(auth) self.head('/auth/tokens', token=token1, - headers={'X-Subject-Token': token2}, expected_status=200) + headers={'X-Subject-Token': token2}, + expected_status=http_client.OK) def test_user_check_other_user_token_rejected(self): # A user cannot check another user's token. @@ -1231,7 +1565,8 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, user_token = self.get_requested_token(user_auth) self.head('/auth/tokens', token=admin_token, - headers={'X-Subject-Token': user_token}, expected_status=200) + headers={'X-Subject-Token': user_token}, + expected_status=http_client.OK) def test_user_revoke_same_token(self): # Given a non-admin user token, the token can be used to revoke @@ -1294,3 +1629,149 @@ class IdentityTestv3CloudPolicySample(test_v3.RestfulTestCase, self.delete('/auth/tokens', token=admin_token, headers={'X-Subject-Token': user_token}) + + def test_user_with_a_role_get_project(self): + user_auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password'], + project_id=self.project['id']) + + # Test user can get project for one they have a role in + self.get('/projects/%s' % self.project['id'], auth=user_auth) + + # Test user can not get project for one they don't have a role in, + # even if they have a role on another project + project2 = unit.new_project_ref(domain_id=self.domainA['id']) + self.resource_api.create_project(project2['id'], project2) + self.get('/projects/%s' % project2['id'], auth=user_auth, + expected_status=exception.ForbiddenAction.code) + + def test_project_admin_get_project(self): + admin_auth = self.build_authentication_request( + user_id=self.project_admin_user['id'], + password=self.project_admin_user['password'], + project_id=self.project['id']) + + resp = self.get('/projects/%s' % self.project['id'], auth=admin_auth) + self.assertEqual(self.project['id'], + jsonutils.loads(resp.body)['project']['id']) + + def test_role_management_no_admin_no_rights(self): + # A non-admin domain user shouldn't be able to manipulate roles + self.auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password'], + domain_id=self.domainA['id']) + + self._role_management_cases(expected=exception.ForbiddenAction.code) + + # ...and nor should non-admin project user + self.auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password'], + project_id=self.project['id']) + + self._role_management_cases(expected=exception.ForbiddenAction.code) + + def test_role_management_with_project_admin(self): + # A project admin user should be able to get and list, but not be able + # to create/update/delete global roles + self.auth = self.build_authentication_request( + user_id=self.project_admin_user['id'], + password=self.project_admin_user['password'], + project_id=self.project['id']) + + self._role_management_cases(read_status_OK=True, + expected=exception.ForbiddenAction.code) + + def test_role_management_with_domain_admin(self): + # A domain admin user should be able to get and list, but not be able + # to create/update/delete global roles + self.auth = self.build_authentication_request( + user_id=self.domain_admin_user['id'], + password=self.domain_admin_user['password'], + domain_id=self.domainA['id']) + + self._role_management_cases(read_status_OK=True, + expected=exception.ForbiddenAction.code) + + def test_role_management_with_cloud_admin(self): + # A cloud admin user should have rights to manipulate global roles + self.auth = self.build_authentication_request( + user_id=self.cloud_admin_user['id'], + password=self.cloud_admin_user['password'], + project_id=self.admin_project['id']) + + self._role_management_cases() + + def test_domain_role_management_no_admin_no_rights(self): + # A non-admin domain user shouldn't be able to manipulate domain roles + self.auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password'], + domain_id=self.domainA['id']) + + self._domain_role_management_cases( + self.domainA['id'], expected=exception.ForbiddenAction.code) + + # ...and nor should non-admin project user + self.auth = self.build_authentication_request( + user_id=self.just_a_user['id'], + password=self.just_a_user['password'], + project_id=self.project['id']) + + self._domain_role_management_cases( + self.domainA['id'], expected=exception.ForbiddenAction.code) + + def test_domain_role_management_with_cloud_admin(self): + # A cloud admin user should have rights to manipulate domain roles + self.auth = self.build_authentication_request( + user_id=self.cloud_admin_user['id'], + password=self.cloud_admin_user['password'], + project_id=self.admin_project['id']) + + self._domain_role_management_cases(self.domainA['id']) + + def test_domain_role_management_with_domain_admin(self): + # A domain admin user should only be able to manipulate the domain + # specific roles in their own domain + self.auth = self.build_authentication_request( + user_id=self.domainB_admin_user['id'], + password=self.domainB_admin_user['password'], + domain_id=self.domainB['id']) + + # Try to access the domain specific roles in another domain + self._domain_role_management_cases( + self.domainA['id'], expected=exception.ForbiddenAction.code) + + # ...but they should be able to work with those in their own domain + self.auth = self.build_authentication_request( + user_id=self.domain_admin_user['id'], + password=self.domain_admin_user['password'], + domain_id=self.domainA['id']) + + self._domain_role_management_cases(self.domainA['id']) + + def test_domain_role_management_with_project_admin(self): + # A project admin user should have not access to domain specific roles + # in another domain. They should be able to get and list domain + # specific roles from their own domain, but not be able to create, + # update or delete them, + self.auth = self.build_authentication_request( + user_id=self.project_adminB_user['id'], + password=self.project_adminB_user['password'], + project_id=self.projectB['id']) + + # Try access the domain specific roless in another domain + self._domain_role_management_cases( + self.domainA['id'], expected=exception.ForbiddenAction.code) + + # ...but they should be ablet to work with those in their own domain + self.auth = self.build_authentication_request( + user_id=self.project_admin_user['id'], + password=self.project_admin_user['password'], + project_id=self.project['id']) + + self._domain_role_management_cases( + self.domainA['id'], read_status_OK=True, + expected=exception.ForbiddenAction.code) diff --git a/keystone-moon/keystone/tests/unit/test_v3_resource.py b/keystone-moon/keystone/tests/unit/test_v3_resource.py new file mode 100644 index 00000000..f54fcb57 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_v3_resource.py @@ -0,0 +1,1434 @@ +# 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 oslo_config import cfg +from six.moves import http_client +from six.moves import range +from testtools import matchers + +from keystone.common import controller +from keystone import exception +from keystone.tests import unit +from keystone.tests.unit import test_v3 +from keystone.tests.unit import utils as test_utils + + +CONF = cfg.CONF + + +class ResourceTestCase(test_v3.RestfulTestCase, + test_v3.AssignmentTestMixin): + """Test domains and projects.""" + + # Domain CRUD tests + + def test_create_domain(self): + """Call ``POST /domains``.""" + ref = unit.new_domain_ref() + r = self.post( + '/domains', + body={'domain': ref}) + return self.assertValidDomainResponse(r, ref) + + def test_create_domain_case_sensitivity(self): + """Call `POST /domains`` twice with upper() and lower() cased name.""" + ref = unit.new_domain_ref() + + # ensure the name is lowercase + ref['name'] = ref['name'].lower() + r = self.post( + '/domains', + body={'domain': ref}) + self.assertValidDomainResponse(r, ref) + + # ensure the name is uppercase + ref['name'] = ref['name'].upper() + r = self.post( + '/domains', + body={'domain': ref}) + self.assertValidDomainResponse(r, ref) + + def test_create_domain_bad_request(self): + """Call ``POST /domains``.""" + self.post('/domains', body={'domain': {}}, + expected_status=http_client.BAD_REQUEST) + + def test_create_domain_unsafe(self): + """Call ``POST /domains with unsafe names``.""" + unsafe_name = 'i am not / safe' + + self.config_fixture.config(group='resource', + domain_name_url_safe='off') + ref = unit.new_domain_ref(name=unsafe_name) + self.post( + '/domains', + body={'domain': ref}) + + for config_setting in ['new', 'strict']: + self.config_fixture.config(group='resource', + domain_name_url_safe=config_setting) + ref = unit.new_domain_ref(name=unsafe_name) + self.post( + '/domains', + body={'domain': ref}, + expected_status=http_client.BAD_REQUEST) + + def test_create_domain_unsafe_default(self): + """Check default for unsafe names for ``POST /domains``.""" + unsafe_name = 'i am not / safe' + + # By default, we should be able to create unsafe names + ref = unit.new_domain_ref(name=unsafe_name) + self.post( + '/domains', + body={'domain': ref}) + + def test_create_domain_creates_is_domain_project(self): + """Check a project that acts as a domain is created. + + Call ``POST /domains``. + """ + # Create a new domain + domain_ref = unit.new_domain_ref() + r = self.post('/domains', body={'domain': domain_ref}) + self.assertValidDomainResponse(r, domain_ref) + + # Retrieve its correspondent project + r = self.get('/projects/%(project_id)s' % { + 'project_id': r.result['domain']['id']}) + self.assertValidProjectResponse(r) + + # The created project has is_domain flag as True + self.assertTrue(r.result['project']['is_domain']) + + # And its parent_id and domain_id attributes are equal + self.assertIsNone(r.result['project']['parent_id']) + self.assertIsNone(r.result['project']['domain_id']) + + def test_create_is_domain_project_creates_domain(self): + """Call ``POST /projects`` is_domain and check a domain is created.""" + # Create a new project that acts as a domain + project_ref = unit.new_project_ref(domain_id=None, is_domain=True) + r = self.post('/projects', body={'project': project_ref}) + self.assertValidProjectResponse(r) + + # Retrieve its correspondent domain + r = self.get('/domains/%(domain_id)s' % { + 'domain_id': r.result['project']['id']}) + self.assertValidDomainResponse(r) + self.assertIsNotNone(r.result['domain']) + + def test_list_domains(self): + """Call ``GET /domains``.""" + resource_url = '/domains' + r = self.get(resource_url) + self.assertValidDomainListResponse(r, ref=self.domain, + resource_url=resource_url) + + def test_get_domain(self): + """Call ``GET /domains/{domain_id}``.""" + r = self.get('/domains/%(domain_id)s' % { + 'domain_id': self.domain_id}) + self.assertValidDomainResponse(r, self.domain) + + def test_update_domain(self): + """Call ``PATCH /domains/{domain_id}``.""" + ref = unit.new_domain_ref() + del ref['id'] + r = self.patch('/domains/%(domain_id)s' % { + 'domain_id': self.domain_id}, + body={'domain': ref}) + self.assertValidDomainResponse(r, ref) + + def test_update_domain_unsafe(self): + """Call ``POST /domains/{domain_id} with unsafe names``.""" + unsafe_name = 'i am not / safe' + + self.config_fixture.config(group='resource', + domain_name_url_safe='off') + ref = unit.new_domain_ref(name=unsafe_name) + del ref['id'] + self.patch('/domains/%(domain_id)s' % { + 'domain_id': self.domain_id}, + body={'domain': ref}) + + unsafe_name = 'i am still not / safe' + for config_setting in ['new', 'strict']: + self.config_fixture.config(group='resource', + domain_name_url_safe=config_setting) + ref = unit.new_domain_ref(name=unsafe_name) + del ref['id'] + self.patch('/domains/%(domain_id)s' % { + 'domain_id': self.domain_id}, + body={'domain': ref}, + expected_status=http_client.BAD_REQUEST) + + def test_update_domain_unsafe_default(self): + """Check default for unsafe names for ``POST /domains``.""" + unsafe_name = 'i am not / safe' + + # By default, we should be able to create unsafe names + ref = unit.new_domain_ref(name=unsafe_name) + del ref['id'] + self.patch('/domains/%(domain_id)s' % { + 'domain_id': self.domain_id}, + body={'domain': ref}) + + def test_update_domain_updates_is_domain_project(self): + """Check the project that acts as a domain is updated. + + Call ``PATCH /domains``. + """ + # Create a new domain + domain_ref = unit.new_domain_ref() + r = self.post('/domains', body={'domain': domain_ref}) + self.assertValidDomainResponse(r, domain_ref) + + # Disable it + self.patch('/domains/%s' % r.result['domain']['id'], + body={'domain': {'enabled': False}}) + + # Retrieve its correspondent project + r = self.get('/projects/%(project_id)s' % { + 'project_id': r.result['domain']['id']}) + self.assertValidProjectResponse(r) + + # The created project is disabled as well + self.assertFalse(r.result['project']['enabled']) + + def test_disable_domain(self): + """Call ``PATCH /domains/{domain_id}`` (set enabled=False).""" + # Create a 2nd set of entities in a 2nd domain + domain2 = unit.new_domain_ref() + self.resource_api.create_domain(domain2['id'], domain2) + + project2 = unit.new_project_ref(domain_id=domain2['id']) + self.resource_api.create_project(project2['id'], project2) + + user2 = unit.create_user(self.identity_api, + domain_id=domain2['id'], + project_id=project2['id']) + + self.assignment_api.add_user_to_project(project2['id'], + user2['id']) + + # First check a user in that domain can authenticate.. + body = { + 'auth': { + 'passwordCredentials': { + 'userId': user2['id'], + 'password': user2['password'] + }, + 'tenantId': project2['id'] + } + } + self.admin_request( + path='/v2.0/tokens', method='POST', body=body) + + auth_data = self.build_authentication_request( + user_id=user2['id'], + password=user2['password'], + project_id=project2['id']) + self.v3_create_token(auth_data) + + # Now disable the domain + domain2['enabled'] = False + r = self.patch('/domains/%(domain_id)s' % { + 'domain_id': domain2['id']}, + body={'domain': {'enabled': False}}) + self.assertValidDomainResponse(r, domain2) + + # Make sure the user can no longer authenticate, via + # either API + body = { + 'auth': { + 'passwordCredentials': { + 'userId': user2['id'], + 'password': user2['password'] + }, + 'tenantId': project2['id'] + } + } + self.admin_request( + path='/v2.0/tokens', method='POST', body=body, + expected_status=http_client.UNAUTHORIZED) + + # Try looking up in v3 by name and id + auth_data = self.build_authentication_request( + user_id=user2['id'], + password=user2['password'], + project_id=project2['id']) + self.v3_create_token(auth_data, + expected_status=http_client.UNAUTHORIZED) + + auth_data = self.build_authentication_request( + username=user2['name'], + user_domain_id=domain2['id'], + password=user2['password'], + project_id=project2['id']) + self.v3_create_token(auth_data, + expected_status=http_client.UNAUTHORIZED) + + def test_delete_enabled_domain_fails(self): + """Call ``DELETE /domains/{domain_id}`` (when domain enabled).""" + # Try deleting an enabled domain, which should fail + self.delete('/domains/%(domain_id)s' % { + 'domain_id': self.domain['id']}, + expected_status=exception.ForbiddenAction.code) + + def test_delete_domain(self): + """Call ``DELETE /domains/{domain_id}``. + + The sample data set up already has a user and project that is part of + self.domain. Additionally we will create a group and a credential + within it. Since the user we will authenticate with is in this domain, + we create a another set of entities in a second domain. Deleting this + second domain should delete all these new entities. In addition, + all the entities in the regular self.domain should be unaffected + by the delete. + + Test Plan: + + - Create domain2 and a 2nd set of entities + - Disable domain2 + - Delete domain2 + - Check entities in domain2 have been deleted + - Check entities in self.domain are unaffected + + """ + # Create a group and a credential in the main domain + group = unit.new_group_ref(domain_id=self.domain_id) + group = self.identity_api.create_group(group) + + credential = unit.new_credential_ref(user_id=self.user['id'], + project_id=self.project_id) + self.credential_api.create_credential(credential['id'], credential) + + # Create a 2nd set of entities in a 2nd domain + domain2 = unit.new_domain_ref() + self.resource_api.create_domain(domain2['id'], domain2) + + project2 = unit.new_project_ref(domain_id=domain2['id']) + project2 = self.resource_api.create_project(project2['id'], project2) + + user2 = unit.new_user_ref(domain_id=domain2['id'], + project_id=project2['id']) + user2 = self.identity_api.create_user(user2) + + group2 = unit.new_group_ref(domain_id=domain2['id']) + group2 = self.identity_api.create_group(group2) + + credential2 = unit.new_credential_ref(user_id=user2['id'], + project_id=project2['id']) + self.credential_api.create_credential(credential2['id'], + credential2) + + # Now disable the new domain and delete it + domain2['enabled'] = False + r = self.patch('/domains/%(domain_id)s' % { + 'domain_id': domain2['id']}, + body={'domain': {'enabled': False}}) + self.assertValidDomainResponse(r, domain2) + self.delete('/domains/%(domain_id)s' % {'domain_id': domain2['id']}) + + # Check all the domain2 relevant entities are gone + self.assertRaises(exception.DomainNotFound, + self.resource_api.get_domain, + domain2['id']) + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + project2['id']) + self.assertRaises(exception.GroupNotFound, + self.identity_api.get_group, + group2['id']) + self.assertRaises(exception.UserNotFound, + self.identity_api.get_user, + user2['id']) + self.assertRaises(exception.CredentialNotFound, + self.credential_api.get_credential, + credential2['id']) + + # ...and that all self.domain entities are still here + r = self.resource_api.get_domain(self.domain['id']) + self.assertDictEqual(self.domain, r) + r = self.resource_api.get_project(self.project['id']) + self.assertDictEqual(self.project, r) + r = self.identity_api.get_group(group['id']) + self.assertDictEqual(group, r) + r = self.identity_api.get_user(self.user['id']) + self.user.pop('password') + self.assertDictEqual(self.user, r) + r = self.credential_api.get_credential(credential['id']) + self.assertDictEqual(credential, r) + + def test_delete_domain_deletes_is_domain_project(self): + """Check the project that acts as a domain is deleted. + + Call ``DELETE /domains``. + """ + # Create a new domain + domain_ref = unit.new_domain_ref() + r = self.post('/domains', body={'domain': domain_ref}) + self.assertValidDomainResponse(r, domain_ref) + + # Retrieve its correspondent project + self.get('/projects/%(project_id)s' % { + 'project_id': r.result['domain']['id']}) + + # Delete the domain + self.patch('/domains/%s' % r.result['domain']['id'], + body={'domain': {'enabled': False}}) + self.delete('/domains/%s' % r.result['domain']['id']) + + # The created project is deleted as well + self.get('/projects/%(project_id)s' % { + 'project_id': r.result['domain']['id']}, expected_status=404) + + def test_delete_default_domain(self): + # Need to disable it first. + self.patch('/domains/%(domain_id)s' % { + 'domain_id': CONF.identity.default_domain_id}, + body={'domain': {'enabled': False}}) + + self.delete( + '/domains/%(domain_id)s' % { + 'domain_id': CONF.identity.default_domain_id}) + + def test_token_revoked_once_domain_disabled(self): + """Test token from a disabled domain has been invalidated. + + Test that a token that was valid for an enabled domain + becomes invalid once that domain is disabled. + + """ + domain = unit.new_domain_ref() + self.resource_api.create_domain(domain['id'], domain) + + user2 = unit.create_user(self.identity_api, + domain_id=domain['id']) + + # build a request body + auth_body = self.build_authentication_request( + user_id=user2['id'], + password=user2['password']) + + # sends a request for the user's token + token_resp = self.post('/auth/tokens', body=auth_body) + + subject_token = token_resp.headers.get('x-subject-token') + + # validates the returned token and it should be valid. + self.head('/auth/tokens', + headers={'x-subject-token': subject_token}, + expected_status=http_client.OK) + + # now disable the domain + domain['enabled'] = False + url = "/domains/%(domain_id)s" % {'domain_id': domain['id']} + self.patch(url, + body={'domain': {'enabled': False}}) + + # validates the same token again and it should be 'not found' + # as the domain has already been disabled. + self.head('/auth/tokens', + headers={'x-subject-token': subject_token}, + expected_status=http_client.NOT_FOUND) + + def test_delete_domain_hierarchy(self): + """Call ``DELETE /domains/{domain_id}``.""" + domain = unit.new_domain_ref() + self.resource_api.create_domain(domain['id'], domain) + + root_project = unit.new_project_ref(domain_id=domain['id']) + root_project = self.resource_api.create_project(root_project['id'], + root_project) + + leaf_project = unit.new_project_ref( + domain_id=domain['id'], + parent_id=root_project['id']) + self.resource_api.create_project(leaf_project['id'], leaf_project) + + # Need to disable it first. + self.patch('/domains/%(domain_id)s' % { + 'domain_id': domain['id']}, + body={'domain': {'enabled': False}}) + + self.delete( + '/domains/%(domain_id)s' % { + 'domain_id': domain['id']}) + + self.assertRaises(exception.DomainNotFound, + self.resource_api.get_domain, + domain['id']) + + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + root_project['id']) + + self.assertRaises(exception.ProjectNotFound, + self.resource_api.get_project, + leaf_project['id']) + + def test_forbid_operations_on_federated_domain(self): + """Make sure one cannot operate on federated domain. + + This includes operations like create, update, delete + on domain identified by id and name where difference variations of + id 'Federated' are used. + + """ + def create_domains(): + for variation in ('Federated', 'FEDERATED', + 'federated', 'fEderated'): + domain = unit.new_domain_ref() + domain['id'] = variation + yield domain + + for domain in create_domains(): + self.assertRaises( + AssertionError, self.resource_api.create_domain, + domain['id'], domain) + self.assertRaises( + AssertionError, self.resource_api.update_domain, + domain['id'], domain) + self.assertRaises( + exception.DomainNotFound, self.resource_api.delete_domain, + domain['id']) + + # swap 'name' with 'id' and try again, expecting the request to + # gracefully fail + domain['id'], domain['name'] = domain['name'], domain['id'] + self.assertRaises( + AssertionError, self.resource_api.create_domain, + domain['id'], domain) + self.assertRaises( + AssertionError, self.resource_api.update_domain, + domain['id'], domain) + self.assertRaises( + exception.DomainNotFound, self.resource_api.delete_domain, + domain['id']) + + def test_forbid_operations_on_defined_federated_domain(self): + """Make sure one cannot operate on a user-defined federated domain. + + This includes operations like create, update, delete. + + """ + non_default_name = 'beta_federated_domain' + self.config_fixture.config(group='federation', + federated_domain_name=non_default_name) + domain = unit.new_domain_ref(name=non_default_name) + self.assertRaises(AssertionError, + self.resource_api.create_domain, + domain['id'], domain) + self.assertRaises(exception.DomainNotFound, + self.resource_api.delete_domain, + domain['id']) + self.assertRaises(AssertionError, + self.resource_api.update_domain, + domain['id'], domain) + + # Project CRUD tests + + def test_list_projects(self): + """Call ``GET /projects``.""" + resource_url = '/projects' + r = self.get(resource_url) + self.assertValidProjectListResponse(r, ref=self.project, + resource_url=resource_url) + + def test_create_project(self): + """Call ``POST /projects``.""" + ref = unit.new_project_ref(domain_id=self.domain_id) + r = self.post( + '/projects', + body={'project': ref}) + self.assertValidProjectResponse(r, ref) + + def test_create_project_bad_request(self): + """Call ``POST /projects``.""" + self.post('/projects', body={'project': {}}, + expected_status=http_client.BAD_REQUEST) + + def test_create_project_invalid_domain_id(self): + """Call ``POST /projects``.""" + ref = unit.new_project_ref(domain_id=uuid.uuid4().hex) + self.post('/projects', body={'project': ref}, + expected_status=http_client.BAD_REQUEST) + + def test_create_project_unsafe(self): + """Call ``POST /projects with unsafe names``.""" + unsafe_name = 'i am not / safe' + + self.config_fixture.config(group='resource', + project_name_url_safe='off') + ref = unit.new_project_ref(name=unsafe_name) + self.post( + '/projects', + body={'project': ref}) + + for config_setting in ['new', 'strict']: + self.config_fixture.config(group='resource', + project_name_url_safe=config_setting) + ref = unit.new_project_ref(name=unsafe_name) + self.post( + '/projects', + body={'project': ref}, + expected_status=http_client.BAD_REQUEST) + + def test_create_project_unsafe_default(self): + """Check default for unsafe names for ``POST /projects``.""" + unsafe_name = 'i am not / safe' + + # By default, we should be able to create unsafe names + ref = unit.new_project_ref(name=unsafe_name) + self.post( + '/projects', + body={'project': ref}) + + def test_create_project_with_parent_id_none_and_domain_id_none(self): + """Call ``POST /projects``.""" + # Grant a domain role for the user + collection_url = ( + '/domains/%(domain_id)s/users/%(user_id)s/roles' % { + 'domain_id': self.domain_id, + 'user_id': self.user['id']}) + member_url = '%(collection_url)s/%(role_id)s' % { + 'collection_url': collection_url, + 'role_id': self.role_id} + self.put(member_url) + + # Create an authentication request for a domain scoped token + auth = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + domain_id=self.domain_id) + + # Without parent_id and domain_id passed as None, the domain_id should + # be normalized to the domain on the token, when using a domain + # scoped token. + ref = unit.new_project_ref() + r = self.post( + '/projects', + auth=auth, + body={'project': ref}) + ref['domain_id'] = self.domain['id'] + self.assertValidProjectResponse(r, ref) + + def test_create_project_without_parent_id_and_without_domain_id(self): + """Call ``POST /projects``.""" + # Grant a domain role for the user + collection_url = ( + '/domains/%(domain_id)s/users/%(user_id)s/roles' % { + 'domain_id': self.domain_id, + 'user_id': self.user['id']}) + member_url = '%(collection_url)s/%(role_id)s' % { + 'collection_url': collection_url, + 'role_id': self.role_id} + self.put(member_url) + + # Create an authentication request for a domain scoped token + auth = self.build_authentication_request( + user_id=self.user['id'], + password=self.user['password'], + domain_id=self.domain_id) + + # Without domain_id and parent_id, the domain_id should be + # normalized to the domain on the token, when using a domain + # scoped token. + ref = unit.new_project_ref() + r = self.post( + '/projects', + auth=auth, + body={'project': ref}) + ref['domain_id'] = self.domain['id'] + self.assertValidProjectResponse(r, ref) + + @test_utils.wip('waiting for support for parent_id to imply domain_id') + def test_create_project_with_parent_id_and_no_domain_id(self): + """Call ``POST /projects``.""" + # With only the parent_id, the domain_id should be + # normalized to the parent's domain_id + ref_child = unit.new_project_ref(parent_id=self.project['id']) + + r = self.post( + '/projects', + body={'project': ref_child}) + self.assertEqual(r.result['project']['domain_id'], + self.project['domain_id']) + ref_child['domain_id'] = self.domain['id'] + self.assertValidProjectResponse(r, ref_child) + + def _create_projects_hierarchy(self, hierarchy_size=1): + """Creates a single-branched project hierarchy with the specified size. + + :param hierarchy_size: the desired hierarchy size, default is 1 - + a project with one child. + + :returns projects: a list of the projects in the created hierarchy. + + """ + new_ref = unit.new_project_ref(domain_id=self.domain_id) + resp = self.post('/projects', body={'project': new_ref}) + + projects = [resp.result] + + for i in range(hierarchy_size): + new_ref = unit.new_project_ref( + domain_id=self.domain_id, + parent_id=projects[i]['project']['id']) + resp = self.post('/projects', + body={'project': new_ref}) + self.assertValidProjectResponse(resp, new_ref) + + projects.append(resp.result) + + return projects + + def test_list_projects_filtering_by_parent_id(self): + """Call ``GET /projects?parent_id={project_id}``.""" + projects = self._create_projects_hierarchy(hierarchy_size=2) + + # Add another child to projects[1] - it will be projects[3] + new_ref = unit.new_project_ref( + domain_id=self.domain_id, + parent_id=projects[1]['project']['id']) + resp = self.post('/projects', + body={'project': new_ref}) + self.assertValidProjectResponse(resp, new_ref) + + projects.append(resp.result) + + # Query for projects[0] immediate children - it will + # be only projects[1] + r = self.get( + '/projects?parent_id=%(project_id)s' % { + 'project_id': projects[0]['project']['id']}) + self.assertValidProjectListResponse(r) + + projects_result = r.result['projects'] + expected_list = [projects[1]['project']] + + # projects[0] has projects[1] as child + self.assertEqual(expected_list, projects_result) + + # Query for projects[1] immediate children - it will + # be projects[2] and projects[3] + r = self.get( + '/projects?parent_id=%(project_id)s' % { + 'project_id': projects[1]['project']['id']}) + self.assertValidProjectListResponse(r) + + projects_result = r.result['projects'] + expected_list = [projects[2]['project'], projects[3]['project']] + + # projects[1] has projects[2] and projects[3] as children + self.assertEqual(expected_list, projects_result) + + # Query for projects[2] immediate children - it will be an empty list + r = self.get( + '/projects?parent_id=%(project_id)s' % { + 'project_id': projects[2]['project']['id']}) + self.assertValidProjectListResponse(r) + + projects_result = r.result['projects'] + expected_list = [] + + # projects[2] has no child, projects_result must be an empty list + self.assertEqual(expected_list, projects_result) + + def test_create_hierarchical_project(self): + """Call ``POST /projects``.""" + self._create_projects_hierarchy() + + def test_get_project(self): + """Call ``GET /projects/{project_id}``.""" + r = self.get( + '/projects/%(project_id)s' % { + 'project_id': self.project_id}) + self.assertValidProjectResponse(r, self.project) + + def test_get_project_with_parents_as_list_with_invalid_id(self): + """Call ``GET /projects/{project_id}?parents_as_list``.""" + self.get('/projects/%(project_id)s?parents_as_list' % { + 'project_id': None}, expected_status=http_client.NOT_FOUND) + + self.get('/projects/%(project_id)s?parents_as_list' % { + 'project_id': uuid.uuid4().hex}, + expected_status=http_client.NOT_FOUND) + + def test_get_project_with_subtree_as_list_with_invalid_id(self): + """Call ``GET /projects/{project_id}?subtree_as_list``.""" + self.get('/projects/%(project_id)s?subtree_as_list' % { + 'project_id': None}, expected_status=http_client.NOT_FOUND) + + self.get('/projects/%(project_id)s?subtree_as_list' % { + 'project_id': uuid.uuid4().hex}, + expected_status=http_client.NOT_FOUND) + + def test_get_project_with_parents_as_ids(self): + """Call ``GET /projects/{project_id}?parents_as_ids``.""" + projects = self._create_projects_hierarchy(hierarchy_size=2) + + # Query for projects[2] parents_as_ids + r = self.get( + '/projects/%(project_id)s?parents_as_ids' % { + 'project_id': projects[2]['project']['id']}) + + self.assertValidProjectResponse(r, projects[2]['project']) + parents_as_ids = r.result['project']['parents'] + + # Assert parents_as_ids is a structured dictionary correctly + # representing the hierarchy. The request was made using projects[2] + # id, hence its parents should be projects[1], projects[0] and the + # is_domain_project, which is the root of the hierarchy. It should + # have the following structure: + # { + # projects[1]: { + # projects[0]: { + # is_domain_project: None + # } + # } + # } + is_domain_project_id = projects[0]['project']['domain_id'] + expected_dict = { + projects[1]['project']['id']: { + projects[0]['project']['id']: {is_domain_project_id: None} + } + } + self.assertDictEqual(expected_dict, parents_as_ids) + + # Query for projects[0] parents_as_ids + r = self.get( + '/projects/%(project_id)s?parents_as_ids' % { + 'project_id': projects[0]['project']['id']}) + + self.assertValidProjectResponse(r, projects[0]['project']) + parents_as_ids = r.result['project']['parents'] + + # projects[0] has only the project that acts as a domain as parent + expected_dict = { + is_domain_project_id: None + } + self.assertDictEqual(expected_dict, parents_as_ids) + + # Query for is_domain_project parents_as_ids + r = self.get( + '/projects/%(project_id)s?parents_as_ids' % { + 'project_id': is_domain_project_id}) + + parents_as_ids = r.result['project']['parents'] + + # the project that acts as a domain has no parents, parents_as_ids + # must be None + self.assertIsNone(parents_as_ids) + + def test_get_project_with_parents_as_list_with_full_access(self): + """``GET /projects/{project_id}?parents_as_list`` with full access. + + Test plan: + + - Create 'parent', 'project' and 'subproject' projects; + - Assign a user a role on each one of those projects; + - Check that calling parents_as_list on 'subproject' returns both + 'project' and 'parent'. + + """ + # Create the project hierarchy + parent, project, subproject = self._create_projects_hierarchy(2) + + # Assign a role for the user on all the created projects + for proj in (parent, project, subproject): + self.put(self.build_role_assignment_link( + role_id=self.role_id, user_id=self.user_id, + project_id=proj['project']['id'])) + + # Make the API call + r = self.get('/projects/%(project_id)s?parents_as_list' % + {'project_id': subproject['project']['id']}) + self.assertValidProjectResponse(r, subproject['project']) + + # Assert only 'project' and 'parent' are in the parents list + self.assertIn(project, r.result['project']['parents']) + self.assertIn(parent, r.result['project']['parents']) + self.assertEqual(2, len(r.result['project']['parents'])) + + def test_get_project_with_parents_as_list_with_partial_access(self): + """``GET /projects/{project_id}?parents_as_list`` with partial access. + + Test plan: + + - Create 'parent', 'project' and 'subproject' projects; + - Assign a user a role on 'parent' and 'subproject'; + - Check that calling parents_as_list on 'subproject' only returns + 'parent'. + + """ + # Create the project hierarchy + parent, project, subproject = self._create_projects_hierarchy(2) + + # Assign a role for the user on parent and subproject + for proj in (parent, subproject): + self.put(self.build_role_assignment_link( + role_id=self.role_id, user_id=self.user_id, + project_id=proj['project']['id'])) + + # Make the API call + r = self.get('/projects/%(project_id)s?parents_as_list' % + {'project_id': subproject['project']['id']}) + self.assertValidProjectResponse(r, subproject['project']) + + # Assert only 'parent' is in the parents list + self.assertIn(parent, r.result['project']['parents']) + self.assertEqual(1, len(r.result['project']['parents'])) + + def test_get_project_with_parents_as_list_and_parents_as_ids(self): + """Attempt to list a project's parents as both a list and as IDs. + + This uses ``GET /projects/{project_id}?parents_as_list&parents_as_ids`` + which should fail with a Bad Request due to the conflicting query + strings. + + """ + projects = self._create_projects_hierarchy(hierarchy_size=2) + + self.get( + '/projects/%(project_id)s?parents_as_list&parents_as_ids' % { + 'project_id': projects[1]['project']['id']}, + expected_status=http_client.BAD_REQUEST) + + def test_list_project_is_domain_filter(self): + """Call ``GET /projects?is_domain=True/False``.""" + # Get the initial number of projects, both acting as a domain as well + # as regular. + r = self.get('/projects?is_domain=True', expected_status=200) + initial_number_is_domain_true = len(r.result['projects']) + r = self.get('/projects?is_domain=False', expected_status=200) + initial_number_is_domain_false = len(r.result['projects']) + + # Add some more projects acting as domains + new_is_domain_project = unit.new_project_ref(is_domain=True) + new_is_domain_project = self.resource_api.create_project( + new_is_domain_project['id'], new_is_domain_project) + new_is_domain_project2 = unit.new_project_ref(is_domain=True) + new_is_domain_project2 = self.resource_api.create_project( + new_is_domain_project2['id'], new_is_domain_project2) + number_is_domain_true = initial_number_is_domain_true + 2 + + r = self.get('/projects?is_domain=True', expected_status=200) + self.assertThat(r.result['projects'], + matchers.HasLength(number_is_domain_true)) + self.assertIn(new_is_domain_project['id'], + [p['id'] for p in r.result['projects']]) + self.assertIn(new_is_domain_project2['id'], + [p['id'] for p in r.result['projects']]) + + # Now add a regular project + new_regular_project = unit.new_project_ref(domain_id=self.domain_id) + new_regular_project = self.resource_api.create_project( + new_regular_project['id'], new_regular_project) + number_is_domain_false = initial_number_is_domain_false + 1 + + # Check we still have the same number of projects acting as domains + r = self.get('/projects?is_domain=True', expected_status=200) + self.assertThat(r.result['projects'], + matchers.HasLength(number_is_domain_true)) + + # Check the number of regular projects is correct + r = self.get('/projects?is_domain=False', expected_status=200) + self.assertThat(r.result['projects'], + matchers.HasLength(number_is_domain_false)) + self.assertIn(new_regular_project['id'], + [p['id'] for p in r.result['projects']]) + + def test_list_project_is_domain_filter_default(self): + """Default project list should not see projects acting as domains""" + # Get the initial count of regular projects + r = self.get('/projects?is_domain=False', expected_status=200) + number_is_domain_false = len(r.result['projects']) + + # Make sure we have at least one project acting as a domain + new_is_domain_project = unit.new_project_ref(is_domain=True) + new_is_domain_project = self.resource_api.create_project( + new_is_domain_project['id'], new_is_domain_project) + + r = self.get('/projects', expected_status=200) + self.assertThat(r.result['projects'], + matchers.HasLength(number_is_domain_false)) + self.assertNotIn(new_is_domain_project, r.result['projects']) + + def test_get_project_with_subtree_as_ids(self): + """Call ``GET /projects/{project_id}?subtree_as_ids``. + + This test creates a more complex hierarchy to test if the structured + dictionary returned by using the ``subtree_as_ids`` query param + correctly represents the hierarchy. + + The hierarchy contains 5 projects with the following structure:: + + +--A--+ + | | + +--B--+ C + | | + D E + + + """ + projects = self._create_projects_hierarchy(hierarchy_size=2) + + # Add another child to projects[0] - it will be projects[3] + new_ref = unit.new_project_ref( + domain_id=self.domain_id, + parent_id=projects[0]['project']['id']) + resp = self.post('/projects', + body={'project': new_ref}) + self.assertValidProjectResponse(resp, new_ref) + projects.append(resp.result) + + # Add another child to projects[1] - it will be projects[4] + new_ref = unit.new_project_ref( + domain_id=self.domain_id, + parent_id=projects[1]['project']['id']) + resp = self.post('/projects', + body={'project': new_ref}) + self.assertValidProjectResponse(resp, new_ref) + projects.append(resp.result) + + # Query for projects[0] subtree_as_ids + r = self.get( + '/projects/%(project_id)s?subtree_as_ids' % { + 'project_id': projects[0]['project']['id']}) + self.assertValidProjectResponse(r, projects[0]['project']) + subtree_as_ids = r.result['project']['subtree'] + + # The subtree hierarchy from projects[0] should have the following + # structure: + # { + # projects[1]: { + # projects[2]: None, + # projects[4]: None + # }, + # projects[3]: None + # } + expected_dict = { + projects[1]['project']['id']: { + projects[2]['project']['id']: None, + projects[4]['project']['id']: None + }, + projects[3]['project']['id']: None + } + self.assertDictEqual(expected_dict, subtree_as_ids) + + # Now query for projects[1] subtree_as_ids + r = self.get( + '/projects/%(project_id)s?subtree_as_ids' % { + 'project_id': projects[1]['project']['id']}) + self.assertValidProjectResponse(r, projects[1]['project']) + subtree_as_ids = r.result['project']['subtree'] + + # The subtree hierarchy from projects[1] should have the following + # structure: + # { + # projects[2]: None, + # projects[4]: None + # } + expected_dict = { + projects[2]['project']['id']: None, + projects[4]['project']['id']: None + } + self.assertDictEqual(expected_dict, subtree_as_ids) + + # Now query for projects[3] subtree_as_ids + r = self.get( + '/projects/%(project_id)s?subtree_as_ids' % { + 'project_id': projects[3]['project']['id']}) + self.assertValidProjectResponse(r, projects[3]['project']) + subtree_as_ids = r.result['project']['subtree'] + + # projects[3] has no subtree, subtree_as_ids must be None + self.assertIsNone(subtree_as_ids) + + def test_get_project_with_subtree_as_list_with_full_access(self): + """``GET /projects/{project_id}?subtree_as_list`` with full access. + + Test plan: + + - Create 'parent', 'project' and 'subproject' projects; + - Assign a user a role on each one of those projects; + - Check that calling subtree_as_list on 'parent' returns both 'parent' + and 'subproject'. + + """ + # Create the project hierarchy + parent, project, subproject = self._create_projects_hierarchy(2) + + # Assign a role for the user on all the created projects + for proj in (parent, project, subproject): + self.put(self.build_role_assignment_link( + role_id=self.role_id, user_id=self.user_id, + project_id=proj['project']['id'])) + + # Make the API call + r = self.get('/projects/%(project_id)s?subtree_as_list' % + {'project_id': parent['project']['id']}) + self.assertValidProjectResponse(r, parent['project']) + + # Assert only 'project' and 'subproject' are in the subtree + self.assertIn(project, r.result['project']['subtree']) + self.assertIn(subproject, r.result['project']['subtree']) + self.assertEqual(2, len(r.result['project']['subtree'])) + + def test_get_project_with_subtree_as_list_with_partial_access(self): + """``GET /projects/{project_id}?subtree_as_list`` with partial access. + + Test plan: + + - Create 'parent', 'project' and 'subproject' projects; + - Assign a user a role on 'parent' and 'subproject'; + - Check that calling subtree_as_list on 'parent' returns 'subproject'. + + """ + # Create the project hierarchy + parent, project, subproject = self._create_projects_hierarchy(2) + + # Assign a role for the user on parent and subproject + for proj in (parent, subproject): + self.put(self.build_role_assignment_link( + role_id=self.role_id, user_id=self.user_id, + project_id=proj['project']['id'])) + + # Make the API call + r = self.get('/projects/%(project_id)s?subtree_as_list' % + {'project_id': parent['project']['id']}) + self.assertValidProjectResponse(r, parent['project']) + + # Assert only 'subproject' is in the subtree + self.assertIn(subproject, r.result['project']['subtree']) + self.assertEqual(1, len(r.result['project']['subtree'])) + + def test_get_project_with_subtree_as_list_and_subtree_as_ids(self): + """Attempt to get a project subtree as both a list and as IDs. + + This uses ``GET /projects/{project_id}?subtree_as_list&subtree_as_ids`` + which should fail with a bad request due to the conflicting query + strings. + + """ + projects = self._create_projects_hierarchy(hierarchy_size=2) + + self.get( + '/projects/%(project_id)s?subtree_as_list&subtree_as_ids' % { + 'project_id': projects[1]['project']['id']}, + expected_status=http_client.BAD_REQUEST) + + def test_update_project(self): + """Call ``PATCH /projects/{project_id}``.""" + ref = unit.new_project_ref(domain_id=self.domain_id, + parent_id=self.project['parent_id']) + del ref['id'] + r = self.patch( + '/projects/%(project_id)s' % { + 'project_id': self.project_id}, + body={'project': ref}) + self.assertValidProjectResponse(r, ref) + + def test_update_project_unsafe(self): + """Call ``POST /projects/{project_id} with unsafe names``.""" + unsafe_name = 'i am not / safe' + + self.config_fixture.config(group='resource', + project_name_url_safe='off') + ref = unit.new_project_ref(name=unsafe_name, + domain_id=self.domain_id, + parent_id=self.project['parent_id']) + del ref['id'] + self.patch( + '/projects/%(project_id)s' % { + 'project_id': self.project_id}, + body={'project': ref}) + + unsafe_name = 'i am still not / safe' + for config_setting in ['new', 'strict']: + self.config_fixture.config(group='resource', + project_name_url_safe=config_setting) + ref = unit.new_project_ref(name=unsafe_name, + domain_id=self.domain_id, + parent_id=self.project['parent_id']) + del ref['id'] + self.patch( + '/projects/%(project_id)s' % { + 'project_id': self.project_id}, + body={'project': ref}, + expected_status=http_client.BAD_REQUEST) + + def test_update_project_unsafe_default(self): + """Check default for unsafe names for ``POST /projects``.""" + unsafe_name = 'i am not / safe' + + # By default, we should be able to create unsafe names + ref = unit.new_project_ref(name=unsafe_name, + domain_id=self.domain_id, + parent_id=self.project['parent_id']) + del ref['id'] + self.patch( + '/projects/%(project_id)s' % { + 'project_id': self.project_id}, + body={'project': ref}) + + def test_update_project_domain_id(self): + """Call ``PATCH /projects/{project_id}`` with domain_id.""" + project = unit.new_project_ref(domain_id=self.domain['id']) + project = self.resource_api.create_project(project['id'], project) + project['domain_id'] = CONF.identity.default_domain_id + r = self.patch('/projects/%(project_id)s' % { + 'project_id': project['id']}, + body={'project': project}, + expected_status=exception.ValidationError.code) + self.config_fixture.config(domain_id_immutable=False) + project['domain_id'] = self.domain['id'] + r = self.patch('/projects/%(project_id)s' % { + 'project_id': project['id']}, + body={'project': project}) + self.assertValidProjectResponse(r, project) + + def test_update_project_parent_id(self): + """Call ``PATCH /projects/{project_id}``.""" + projects = self._create_projects_hierarchy() + leaf_project = projects[1]['project'] + leaf_project['parent_id'] = None + self.patch( + '/projects/%(project_id)s' % { + 'project_id': leaf_project['id']}, + body={'project': leaf_project}, + expected_status=http_client.FORBIDDEN) + + def test_update_project_is_domain_not_allowed(self): + """Call ``PATCH /projects/{project_id}`` with is_domain. + + The is_domain flag is immutable. + """ + project = unit.new_project_ref(domain_id=self.domain['id']) + resp = self.post('/projects', + body={'project': project}) + self.assertFalse(resp.result['project']['is_domain']) + + project['parent_id'] = resp.result['project']['parent_id'] + project['is_domain'] = True + self.patch('/projects/%(project_id)s' % { + 'project_id': resp.result['project']['id']}, + body={'project': project}, + expected_status=http_client.BAD_REQUEST) + + def test_disable_leaf_project(self): + """Call ``PATCH /projects/{project_id}``.""" + projects = self._create_projects_hierarchy() + leaf_project = projects[1]['project'] + leaf_project['enabled'] = False + r = self.patch( + '/projects/%(project_id)s' % { + 'project_id': leaf_project['id']}, + body={'project': leaf_project}) + self.assertEqual( + leaf_project['enabled'], r.result['project']['enabled']) + + def test_disable_not_leaf_project(self): + """Call ``PATCH /projects/{project_id}``.""" + projects = self._create_projects_hierarchy() + root_project = projects[0]['project'] + root_project['enabled'] = False + self.patch( + '/projects/%(project_id)s' % { + 'project_id': root_project['id']}, + body={'project': root_project}, + expected_status=http_client.FORBIDDEN) + + def test_delete_project(self): + """Call ``DELETE /projects/{project_id}`` + + As well as making sure the delete succeeds, we ensure + that any credentials that reference this projects are + also deleted, while other credentials are unaffected. + + """ + credential = unit.new_credential_ref(user_id=self.user['id'], + project_id=self.project_id) + self.credential_api.create_credential(credential['id'], credential) + + # First check the credential for this project is present + r = self.credential_api.get_credential(credential['id']) + self.assertDictEqual(credential, r) + # Create a second credential with a different project + project2 = unit.new_project_ref(domain_id=self.domain['id']) + self.resource_api.create_project(project2['id'], project2) + credential2 = unit.new_credential_ref(user_id=self.user['id'], + project_id=project2['id']) + self.credential_api.create_credential(credential2['id'], credential2) + + # Now delete the project + self.delete( + '/projects/%(project_id)s' % { + 'project_id': self.project_id}) + + # Deleting the project should have deleted any credentials + # that reference this project + self.assertRaises(exception.CredentialNotFound, + self.credential_api.get_credential, + credential_id=credential['id']) + # But the credential for project2 is unaffected + r = self.credential_api.get_credential(credential2['id']) + self.assertDictEqual(credential2, r) + + def test_delete_not_leaf_project(self): + """Call ``DELETE /projects/{project_id}``.""" + projects = self._create_projects_hierarchy() + self.delete( + '/projects/%(project_id)s' % { + 'project_id': projects[0]['project']['id']}, + expected_status=http_client.FORBIDDEN) + + +class ResourceV3toV2MethodsTestCase(unit.TestCase): + """Test domain V3 to V2 conversion methods.""" + + def _setup_initial_projects(self): + self.project_id = uuid.uuid4().hex + self.domain_id = CONF.identity.default_domain_id + self.parent_id = uuid.uuid4().hex + # Project with only domain_id in ref + self.project1 = unit.new_project_ref(id=self.project_id, + name=self.project_id, + domain_id=self.domain_id) + # Project with both domain_id and parent_id in ref + self.project2 = unit.new_project_ref(id=self.project_id, + name=self.project_id, + domain_id=self.domain_id, + parent_id=self.parent_id) + # Project with no domain_id and parent_id in ref + self.project3 = unit.new_project_ref(id=self.project_id, + name=self.project_id, + domain_id=self.domain_id, + parent_id=self.parent_id) + # Expected result with no domain_id and parent_id + self.expected_project = {'id': self.project_id, + 'name': self.project_id} + + def test_v2controller_filter_domain_id(self): + # V2.0 is not domain aware, ensure domain_id is popped off the ref. + other_data = uuid.uuid4().hex + domain_id = CONF.identity.default_domain_id + ref = {'domain_id': domain_id, + 'other_data': other_data} + + ref_no_domain = {'other_data': other_data} + expected_ref = ref_no_domain.copy() + + updated_ref = controller.V2Controller.filter_domain_id(ref) + self.assertIs(ref, updated_ref) + self.assertDictEqual(expected_ref, ref) + # Make sure we don't error/muck up data if domain_id isn't present + updated_ref = controller.V2Controller.filter_domain_id(ref_no_domain) + self.assertIs(ref_no_domain, updated_ref) + self.assertDictEqual(expected_ref, ref_no_domain) + + def test_v3controller_filter_domain_id(self): + # No data should be filtered out in this case. + other_data = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + ref = {'domain_id': domain_id, + 'other_data': other_data} + + expected_ref = ref.copy() + updated_ref = controller.V3Controller.filter_domain_id(ref) + self.assertIs(ref, updated_ref) + self.assertDictEqual(expected_ref, ref) + + def test_v2controller_filter_domain(self): + other_data = uuid.uuid4().hex + domain_id = uuid.uuid4().hex + non_default_domain_ref = {'domain': {'id': domain_id}, + 'other_data': other_data} + default_domain_ref = {'domain': {'id': 'default'}, + 'other_data': other_data} + updated_ref = controller.V2Controller.filter_domain(default_domain_ref) + self.assertNotIn('domain', updated_ref) + self.assertNotIn( + 'domain', + controller.V2Controller.filter_domain(non_default_domain_ref)) + + def test_v2controller_filter_project_parent_id(self): + # V2.0 is not project hierarchy aware, ensure parent_id is popped off. + other_data = uuid.uuid4().hex + parent_id = uuid.uuid4().hex + ref = {'parent_id': parent_id, + 'other_data': other_data} + + ref_no_parent = {'other_data': other_data} + expected_ref = ref_no_parent.copy() + + updated_ref = controller.V2Controller.filter_project_parent_id(ref) + self.assertIs(ref, updated_ref) + self.assertDictEqual(expected_ref, ref) + # Make sure we don't error/muck up data if parent_id isn't present + updated_ref = controller.V2Controller.filter_project_parent_id( + ref_no_parent) + self.assertIs(ref_no_parent, updated_ref) + self.assertDictEqual(expected_ref, ref_no_parent) + + def test_v3_to_v2_project_method(self): + self._setup_initial_projects() + + # TODO(shaleh): these optional fields are not handled well by the + # v3_to_v2 code. Manually remove them for now. Eventually update + # new_project_ref to not return optional values + del self.project1['enabled'] + del self.project1['description'] + del self.project2['enabled'] + del self.project2['description'] + del self.project3['enabled'] + del self.project3['description'] + + updated_project1 = controller.V2Controller.v3_to_v2_project( + self.project1) + self.assertIs(self.project1, updated_project1) + self.assertDictEqual(self.expected_project, self.project1) + updated_project2 = controller.V2Controller.v3_to_v2_project( + self.project2) + self.assertIs(self.project2, updated_project2) + self.assertDictEqual(self.expected_project, self.project2) + updated_project3 = controller.V2Controller.v3_to_v2_project( + self.project3) + self.assertIs(self.project3, updated_project3) + self.assertDictEqual(self.expected_project, self.project2) + + def test_v3_to_v2_project_method_list(self): + self._setup_initial_projects() + project_list = [self.project1, self.project2, self.project3] + + # TODO(shaleh): these optional fields are not handled well by the + # v3_to_v2 code. Manually remove them for now. Eventually update + # new_project_ref to not return optional values + for p in project_list: + del p['enabled'] + del p['description'] + updated_list = controller.V2Controller.v3_to_v2_project(project_list) + + self.assertEqual(len(updated_list), len(project_list)) + + for i, ref in enumerate(updated_list): + # Order should not change. + self.assertIs(ref, project_list[i]) + + self.assertDictEqual(self.expected_project, self.project1) + self.assertDictEqual(self.expected_project, self.project2) + self.assertDictEqual(self.expected_project, self.project3) diff --git a/keystone-moon/keystone/tests/unit/test_v3_trust.py b/keystone-moon/keystone/tests/unit/test_v3_trust.py new file mode 100644 index 00000000..d3127c89 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/test_v3_trust.py @@ -0,0 +1,403 @@ +# 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 datetime +import uuid + +from six.moves import http_client + +from keystone.tests import unit +from keystone.tests.unit import test_v3 + + +class TestTrustOperations(test_v3.RestfulTestCase): + """Test module for create, read, update and delete operations on trusts. + + This module is specific to tests for trust CRUD operations. All other tests + related to trusts that are authentication or authorization specific should + live in in the keystone/tests/unit/test_v3_auth.py module. + + """ + + def setUp(self): + super(TestTrustOperations, self).setUp() + # create a trustee to delegate stuff to + self.trustee_user = unit.create_user(self.identity_api, + domain_id=self.domain_id) + self.trustee_user_id = self.trustee_user['id'] + + def test_create_trust_bad_request(self): + # The server returns a 403 Forbidden rather than a 400 Bad Request, see + # bug 1133435 + self.post('/OS-TRUST/trusts', body={'trust': {}}, + expected_status=http_client.FORBIDDEN) + + def test_trust_crud(self): + # create a new trust + ref = unit.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + role_ids=[self.role_id]) + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + trust = self.assertValidTrustResponse(r, ref) + + # get the trust + r = self.get( + '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}) + self.assertValidTrustResponse(r, ref) + + # validate roles on the trust + r = self.get( + '/OS-TRUST/trusts/%(trust_id)s/roles' % { + 'trust_id': trust['id']}) + roles = self.assertValidRoleListResponse(r, self.role) + self.assertIn(self.role['id'], [x['id'] for x in roles]) + self.head( + '/OS-TRUST/trusts/%(trust_id)s/roles/%(role_id)s' % { + 'trust_id': trust['id'], + 'role_id': self.role['id']}, + expected_status=http_client.OK) + r = self.get( + '/OS-TRUST/trusts/%(trust_id)s/roles/%(role_id)s' % { + 'trust_id': trust['id'], + 'role_id': self.role['id']}) + self.assertValidRoleResponse(r, self.role) + + # list all trusts + r = self.get('/OS-TRUST/trusts') + self.assertValidTrustListResponse(r, trust) + + # trusts are immutable + self.patch( + '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}, + body={'trust': ref}, + expected_status=http_client.NOT_FOUND) + + # delete the trust + self.delete( + '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}) + + # ensure the trust is not found + self.get( + '/OS-TRUST/trusts/%(trust_id)s' % {'trust_id': trust['id']}, + expected_status=http_client.NOT_FOUND) + + def test_list_trusts(self): + # create three trusts with the same trustor and trustee + ref = unit.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + impersonation=False, + expires=dict(minutes=1), + role_ids=[self.role_id]) + for i in range(3): + ref['expires_at'] = datetime.datetime.utcnow().replace( + year=2032).strftime(unit.TIME_FORMAT) + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + self.assertValidTrustResponse(r, ref) + + # list all trusts + r = self.get('/OS-TRUST/trusts') + trusts = r.result['trusts'] + self.assertEqual(3, len(trusts)) + self.assertValidTrustListResponse(r) + + # list all trusts for the trustor + r = self.get('/OS-TRUST/trusts?trustor_user_id=%s' % + self.user_id) + trusts = r.result['trusts'] + self.assertEqual(3, len(trusts)) + self.assertValidTrustListResponse(r) + + # list all trusts as the trustor as the trustee. + r = self.get('/OS-TRUST/trusts?trustee_user_id=%s' % + self.user_id) + trusts = r.result['trusts'] + self.assertEqual(0, len(trusts)) + + # list all trusts as the trustee is forbidden + r = self.get('/OS-TRUST/trusts?trustee_user_id=%s' % + self.trustee_user_id, + expected_status=http_client.FORBIDDEN) + + def test_delete_trust(self): + # create a trust + ref = unit.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + impersonation=False, + expires=dict(minutes=1), + role_ids=[self.role_id]) + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + trust = self.assertValidTrustResponse(r, ref) + + # delete the trust + self.delete('/OS-TRUST/trusts/%(trust_id)s' % { + 'trust_id': trust['id']}) + + # ensure the trust isn't found + self.get('/OS-TRUST/trusts/%(trust_id)s' % { + 'trust_id': trust['id']}, + expected_status=http_client.NOT_FOUND) + + def test_create_trust_without_trustee_returns_bad_request(self): + ref = unit.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + role_ids=[self.role_id]) + + # trustee_user_id is required to create a trust + del ref['trustee_user_id'] + + self.post('/OS-TRUST/trusts', + body={'trust': ref}, + expected_status=http_client.BAD_REQUEST) + + def test_create_trust_without_impersonation_returns_bad_request(self): + ref = unit.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + role_ids=[self.role_id]) + + # impersonation is required to create a trust + del ref['impersonation'] + + self.post('/OS-TRUST/trusts', + body={'trust': ref}, + expected_status=http_client.BAD_REQUEST) + + def test_create_trust_with_bad_remaining_uses_returns_bad_request(self): + # negative numbers, strings, non-integers, and 0 are not value values + for value in [-1, 0, "a bad value", 7.2]: + ref = unit.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + remaining_uses=value, + role_ids=[self.role_id]) + self.post('/OS-TRUST/trusts', + body={'trust': ref}, + expected_status=http_client.BAD_REQUEST) + + def test_create_trust_with_non_existant_trustee_returns_not_found(self): + ref = unit.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=uuid.uuid4().hex, + project_id=self.project_id, + role_ids=[self.role_id]) + self.post('/OS-TRUST/trusts', body={'trust': ref}, + expected_status=http_client.NOT_FOUND) + + def test_create_trust_with_trustee_as_trustor_returns_forbidden(self): + ref = unit.new_trust_ref( + trustor_user_id=self.trustee_user_id, + trustee_user_id=self.user_id, + project_id=self.project_id, + role_ids=[self.role_id]) + # NOTE(lbragstad): This fails because the user making the request isn't + # the trustor defined in the request. + self.post('/OS-TRUST/trusts', body={'trust': ref}, + expected_status=http_client.FORBIDDEN) + + def test_create_trust_with_non_existant_project_returns_not_found(self): + ref = unit.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=uuid.uuid4().hex, + role_ids=[self.role_id]) + self.post('/OS-TRUST/trusts', body={'trust': ref}, + expected_status=http_client.NOT_FOUND) + + def test_create_trust_with_non_existant_role_id_returns_not_found(self): + ref = unit.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + role_ids=[uuid.uuid4().hex]) + self.post('/OS-TRUST/trusts', body={'trust': ref}, + expected_status=http_client.NOT_FOUND) + + def test_create_trust_with_non_existant_role_name_returns_not_found(self): + ref = unit.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + role_names=[uuid.uuid4().hex]) + self.post('/OS-TRUST/trusts', body={'trust': ref}, + expected_status=http_client.NOT_FOUND) + + def test_validate_trust_scoped_token_against_v2_returns_unauthorized(self): + # create a new trust + ref = unit.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.default_domain_user_id, + project_id=self.project_id, + impersonation=False, + expires=dict(minutes=1), + role_ids=[self.role_id]) + r = self.post('/OS-TRUST/trusts', body={'trust': ref}) + trust = self.assertValidTrustResponse(r) + + # get a v3 trust-scoped token as the trustee + auth_data = self.build_authentication_request( + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password'], + trust_id=trust['id']) + r = self.v3_create_token(auth_data) + self.assertValidProjectScopedTokenResponse( + r, self.default_domain_user) + token = r.headers.get('X-Subject-Token') + + # now validate the v3 token with v2 API + path = '/v2.0/tokens/%s' % (token) + self.admin_request( + path=path, token=self.get_admin_token(), + method='GET', expected_status=http_client.UNAUTHORIZED) + + def test_v3_v2_intermix_trustor_not_in_default_domain_failed(self): + # get a project-scoped token + auth_data = self.build_authentication_request( + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password'], + project_id=self.default_domain_project_id) + token = self.get_requested_token(auth_data) + + # create a new trust + ref = unit.new_trust_ref( + trustor_user_id=self.default_domain_user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.default_domain_project_id, + impersonation=False, + expires=dict(minutes=1), + role_ids=[self.role_id]) + r = self.post('/OS-TRUST/trusts', body={'trust': ref}, token=token) + trust = self.assertValidTrustResponse(r) + + # get a trust-scoped token as the trustee + auth_data = self.build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password'], + trust_id=trust['id']) + r = self.v3_create_token(auth_data) + self.assertValidProjectScopedTokenResponse( + r, self.trustee_user) + token = r.headers.get('X-Subject-Token') + + # now validate the v3 token with v2 API + path = '/v2.0/tokens/%s' % (token) + self.admin_request( + path=path, token=self.get_admin_token(), + method='GET', expected_status=http_client.UNAUTHORIZED) + + def test_v3_v2_intermix_project_not_in_default_domain_failed(self): + # create a trustee in default domain to delegate stuff to + trustee_user = unit.create_user(self.identity_api, + domain_id=test_v3.DEFAULT_DOMAIN_ID) + trustee_user_id = trustee_user['id'] + + # create a new trust + ref = unit.new_trust_ref( + trustor_user_id=self.default_domain_user_id, + trustee_user_id=trustee_user_id, + project_id=self.project_id, + impersonation=False, + expires=dict(minutes=1), + role_ids=[self.role_id]) + + # get a project-scoped token as the default_domain_user + auth_data = self.build_authentication_request( + user_id=self.default_domain_user['id'], + password=self.default_domain_user['password'], + project_id=self.default_domain_project_id) + token = self.get_requested_token(auth_data) + + r = self.post('/OS-TRUST/trusts', body={'trust': ref}, token=token) + trust = self.assertValidTrustResponse(r) + + # get a trust-scoped token as the trustee + auth_data = self.build_authentication_request( + user_id=trustee_user['id'], + password=trustee_user['password'], + trust_id=trust['id']) + r = self.v3_create_token(auth_data) + self.assertValidProjectScopedTokenResponse(r, trustee_user) + token = r.headers.get('X-Subject-Token') + + # ensure the token is invalid against v2 + path = '/v2.0/tokens/%s' % (token) + self.admin_request( + path=path, token=self.get_admin_token(), + method='GET', expected_status=http_client.UNAUTHORIZED) + + def test_exercise_trust_scoped_token_without_impersonation(self): + # create a new trust + ref = unit.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + impersonation=False, + expires=dict(minutes=1), + role_ids=[self.role_id]) + resp = self.post('/OS-TRUST/trusts', body={'trust': ref}) + trust = self.assertValidTrustResponse(resp) + + # get a trust-scoped token as the trustee + auth_data = self.build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password'], + trust_id=trust['id']) + resp = self.v3_create_token(auth_data) + resp_body = resp.json_body['token'] + + self.assertValidProjectScopedTokenResponse(resp, + self.trustee_user) + self.assertEqual(self.trustee_user['id'], resp_body['user']['id']) + self.assertEqual(self.trustee_user['name'], resp_body['user']['name']) + self.assertEqual(self.domain['id'], resp_body['user']['domain']['id']) + self.assertEqual(self.domain['name'], + resp_body['user']['domain']['name']) + self.assertEqual(self.project['id'], resp_body['project']['id']) + self.assertEqual(self.project['name'], resp_body['project']['name']) + + def test_exercise_trust_scoped_token_with_impersonation(self): + # create a new trust + ref = unit.new_trust_ref( + trustor_user_id=self.user_id, + trustee_user_id=self.trustee_user_id, + project_id=self.project_id, + impersonation=True, + expires=dict(minutes=1), + role_ids=[self.role_id]) + resp = self.post('/OS-TRUST/trusts', body={'trust': ref}) + trust = self.assertValidTrustResponse(resp) + + # get a trust-scoped token as the trustee + auth_data = self.build_authentication_request( + user_id=self.trustee_user['id'], + password=self.trustee_user['password'], + trust_id=trust['id']) + resp = self.v3_create_token(auth_data) + resp_body = resp.json_body['token'] + + self.assertValidProjectScopedTokenResponse(resp, self.user) + self.assertEqual(self.user['id'], resp_body['user']['id']) + self.assertEqual(self.user['name'], resp_body['user']['name']) + self.assertEqual(self.domain['id'], resp_body['user']['domain']['id']) + self.assertEqual(self.domain['name'], + resp_body['user']['domain']['name']) + self.assertEqual(self.project['id'], resp_body['project']['id']) + self.assertEqual(self.project['name'], resp_body['project']['name']) diff --git a/keystone-moon/keystone/tests/unit/test_validation.py b/keystone-moon/keystone/tests/unit/test_validation.py index f7a224a0..73cb6ef6 100644 --- a/keystone-moon/keystone/tests/unit/test_validation.py +++ b/keystone-moon/keystone/tests/unit/test_validation.py @@ -21,11 +21,11 @@ from keystone.catalog import schema as catalog_schema from keystone.common import validation from keystone.common.validation import parameter_types from keystone.common.validation import validators -from keystone.contrib.endpoint_filter import schema as endpoint_filter_schema -from keystone.contrib.federation import schema as federation_schema from keystone.credential import schema as credential_schema from keystone import exception +from keystone.federation import schema as federation_schema from keystone.identity import schema as identity_schema +from keystone.oauth1 import schema as oauth1_schema from keystone.policy import schema as policy_schema from keystone.resource import schema as resource_schema from keystone.tests import unit @@ -67,6 +67,12 @@ entity_create = { 'additionalProperties': True, } +entity_create_optional_body = { + 'type': 'object', + 'properties': _entity_properties, + 'additionalProperties': True, +} + entity_update = { 'type': 'object', 'properties': _entity_properties, @@ -78,6 +84,8 @@ _VALID_ENABLED_FORMATS = [True, False] _INVALID_ENABLED_FORMATS = ['some string', 1, 0, 'True', 'False'] +_INVALID_DESC_FORMATS = [False, 1, 2.0] + _VALID_URLS = ['https://example.com', 'http://EXAMPLE.com/v3', 'http://localhost', 'http://127.0.0.1:5000', 'http://1.1.1.1', 'http://255.255.255.255', @@ -90,7 +98,7 @@ _VALID_URLS = ['https://example.com', 'http://EXAMPLE.com/v3', _INVALID_URLS = [False, 'this is not a URL', 1234, 'www.example.com', 'localhost', 'http//something.com', - 'https//something.com'] + 'https//something.com', ' http://example.com'] _VALID_FILTERS = [{'interface': 'admin'}, {'region': 'US-WEST', @@ -99,6 +107,17 @@ _VALID_FILTERS = [{'interface': 'admin'}, _INVALID_FILTERS = ['some string', 1, 0, True, False] +def expected_validation_failure(msg): + def wrapper(f): + def wrapped(self, *args, **kwargs): + args = (self,) + args + e = self.assertRaises(exception.ValidationError, f, + *args, **kwargs) + self.assertIn(msg, six.text_type(e)) + return wrapped + return wrapper + + class ValidatedDecoratorTests(unit.BaseTestCase): entity_schema = { @@ -113,42 +132,51 @@ class ValidatedDecoratorTests(unit.BaseTestCase): 'name': uuid.uuid4().hex, } - invalid_entity = {} - - @validation.validated(entity_schema, 'entity') - def do_something(self, entity): - pass + invalid_entity = { + 'name': 1.0, # NOTE(dstanek): this is the incorrect type for name + } @validation.validated(entity_create, 'entity') def create_entity(self, entity): - pass + """Used to test cases where validated param is the only param.""" + + @validation.validated(entity_create_optional_body, 'entity') + def create_entity_optional_body(self, entity): + """Used to test cases where there is an optional body.""" @validation.validated(entity_update, 'entity') def update_entity(self, entity_id, entity): - pass + """Used to test cases where validated param is not the only param.""" - def _assert_call_entity_method_fails(self, method, *args, **kwargs): - e = self.assertRaises(exception.ValidationError, method, - *args, **kwargs) + def test_calling_create_with_valid_entity_kwarg_succeeds(self): + self.create_entity(entity=self.valid_entity) - self.assertIn('Expecting to find entity in request body', - six.text_type(e)) + def test_calling_create_with_empty_entity_kwarg_succeeds(self): + """Test the case when client passing in an empty kwarg reference.""" + self.create_entity_optional_body(entity={}) - def test_calling_with_valid_entity_kwarg_succeeds(self): - self.do_something(entity=self.valid_entity) + @expected_validation_failure('Expecting to find entity in request body') + def test_calling_create_with_kwarg_as_None_fails(self): + self.create_entity(entity=None) - def test_calling_with_invalid_entity_kwarg_fails(self): - self.assertRaises(exception.ValidationError, - self.do_something, - entity=self.invalid_entity) + def test_calling_create_with_valid_entity_arg_succeeds(self): + self.create_entity(self.valid_entity) - def test_calling_with_valid_entity_arg_succeeds(self): - self.do_something(self.valid_entity) + def test_calling_create_with_empty_entity_arg_succeeds(self): + """Test the case when client passing in an empty entity reference.""" + self.create_entity_optional_body({}) - def test_calling_with_invalid_entity_arg_fails(self): - self.assertRaises(exception.ValidationError, - self.do_something, - self.invalid_entity) + @expected_validation_failure("Invalid input for field 'name'") + def test_calling_create_with_invalid_entity_fails(self): + self.create_entity(self.invalid_entity) + + @expected_validation_failure('Expecting to find entity in request body') + def test_calling_create_with_entity_arg_as_None_fails(self): + self.create_entity(None) + + @expected_validation_failure('Expecting to find entity in request body') + def test_calling_create_without_an_entity_fails(self): + self.create_entity() def test_using_the_wrong_name_with_the_decorator_fails(self): with testtools.ExpectedException(TypeError): @@ -156,24 +184,26 @@ class ValidatedDecoratorTests(unit.BaseTestCase): def function(entity): pass - def test_create_entity_no_request_body_with_decorator(self): - """Test the case when request body is not provided.""" - self._assert_call_entity_method_fails(self.create_entity) + # NOTE(dstanek): below are the test cases for making sure the validation + # works when the validated param is not the only param. Since all of the + # actual validation cases are tested above these test are for a sanity + # check. - def test_create_entity_empty_request_body_with_decorator(self): - """Test the case when client passing in an empty entity reference.""" - self._assert_call_entity_method_fails(self.create_entity, entity={}) + def test_calling_update_with_valid_entity_succeeds(self): + self.update_entity(uuid.uuid4().hex, self.valid_entity) - def test_update_entity_no_request_body_with_decorator(self): - """Test the case when request body is not provided.""" - self._assert_call_entity_method_fails(self.update_entity, - uuid.uuid4().hex) + @expected_validation_failure("Invalid input for field 'name'") + def test_calling_update_with_invalid_entity_fails(self): + self.update_entity(uuid.uuid4().hex, self.invalid_entity) - def test_update_entity_empty_request_body_with_decorator(self): + def test_calling_update_with_empty_entity_kwarg_succeeds(self): """Test the case when client passing in an empty entity reference.""" - self._assert_call_entity_method_fails(self.update_entity, - uuid.uuid4().hex, - entity={}) + global entity_update + original_entity_update = entity_update.copy() + # pop 'minProperties' from schema so that empty body is allowed. + entity_update.pop('minProperties') + self.update_entity(uuid.uuid4().hex, entity={}) + entity_update = original_entity_update class EntityValidationTestCase(unit.BaseTestCase): @@ -499,11 +529,22 @@ class ProjectValidationTestCase(unit.BaseTestCase): self.update_project_validator.validate, request_to_validate) - def test_validate_project_update_request_with_null_domain_id_fails(self): - request_to_validate = {'domain_id': None} - self.assertRaises(exception.SchemaValidationError, - self.update_project_validator.validate, - request_to_validate) + def test_validate_project_create_request_with_valid_domain_id(self): + """Test that we validate `domain_id` in create project requests.""" + # domain_id is nullable + for domain_id in [None, uuid.uuid4().hex]: + request_to_validate = {'name': self.project_name, + 'domain_id': domain_id} + self.create_project_validator.validate(request_to_validate) + + def test_validate_project_request_with_invalid_domain_id_fails(self): + """Exception is raised when `domain_id` is a non-id value.""" + for domain_id in [False, 'fake_project']: + request_to_validate = {'name': self.project_name, + 'domain_id': domain_id} + self.assertRaises(exception.SchemaValidationError, + self.create_project_validator.validate, + request_to_validate) class DomainValidationTestCase(unit.BaseTestCase): @@ -897,6 +938,11 @@ class RegionValidationTestCase(unit.BaseTestCase): request_to_validate = {'other_attr': uuid.uuid4().hex} self.create_region_validator.validate(request_to_validate) + def test_validate_region_create_succeeds_with_no_parameters(self): + """Validate create region request with no parameters.""" + request_to_validate = {} + self.create_region_validator.validate(request_to_validate) + def test_validate_region_update_succeeds(self): """Test that we validate a region update request.""" request_to_validate = {'id': 'us-west', @@ -1298,8 +1344,8 @@ class EndpointGroupValidationTestCase(unit.BaseTestCase): def setUp(self): super(EndpointGroupValidationTestCase, self).setUp() - create = endpoint_filter_schema.endpoint_group_create - update = endpoint_filter_schema.endpoint_group_update + create = catalog_schema.endpoint_group_create + update = catalog_schema.endpoint_group_update self.create_endpoint_grp_validator = validators.SchemaValidator(create) self.update_endpoint_grp_validator = validators.SchemaValidator(update) @@ -1321,8 +1367,7 @@ class EndpointGroupValidationTestCase(unit.BaseTestCase): self.create_endpoint_grp_validator.validate(request_to_validate) def test_validate_endpoint_group_create_succeeds_with_valid_filters(self): - """Validate dict values as `filters` in endpoint group create requests. - """ + """Validate `filters` in endpoint group create requests.""" request_to_validate = {'description': 'endpoint group description', 'name': 'endpoint_group_name'} for valid_filters in _VALID_FILTERS: @@ -1718,13 +1763,8 @@ class UserValidationTestCase(unit.BaseTestCase): def test_validate_user_create_with_all_valid_parameters_succeeds(self): """Test that validating a user create request succeeds.""" - request_to_validate = {'name': self.user_name, - 'default_project_id': uuid.uuid4().hex, - 'domain_id': uuid.uuid4().hex, - 'description': uuid.uuid4().hex, - 'enabled': True, - 'email': uuid.uuid4().hex, - 'password': uuid.uuid4().hex} + request_to_validate = unit.new_user_ref(domain_id=uuid.uuid4().hex, + name=self.user_name) self.create_user_validator.validate(request_to_validate) def test_validate_user_create_fails_without_name(self): @@ -1875,3 +1915,201 @@ class GroupValidationTestCase(unit.BaseTestCase): """Validate group update requests with extra parameters.""" request_to_validate = {'other_attr': uuid.uuid4().hex} self.update_group_validator.validate(request_to_validate) + + +class IdentityProviderValidationTestCase(unit.BaseTestCase): + """Test for V3 Identity Provider API validation.""" + + def setUp(self): + super(IdentityProviderValidationTestCase, self).setUp() + + create = federation_schema.identity_provider_create + update = federation_schema.identity_provider_update + self.create_idp_validator = validators.SchemaValidator(create) + self.update_idp_validator = validators.SchemaValidator(update) + + def test_validate_idp_request_succeeds(self): + """Test that we validate an identity provider request.""" + request_to_validate = {'description': 'identity provider description', + 'enabled': True, + 'remote_ids': [uuid.uuid4().hex, + uuid.uuid4().hex]} + self.create_idp_validator.validate(request_to_validate) + self.update_idp_validator.validate(request_to_validate) + + def test_validate_idp_request_fails_with_invalid_params(self): + """Exception raised when unknown parameter is found.""" + request_to_validate = {'bogus': uuid.uuid4().hex} + self.assertRaises(exception.SchemaValidationError, + self.create_idp_validator.validate, + request_to_validate) + + self.assertRaises(exception.SchemaValidationError, + self.update_idp_validator.validate, + request_to_validate) + + def test_validate_idp_request_with_enabled(self): + """Validate `enabled` as boolean-like values.""" + for valid_enabled in _VALID_ENABLED_FORMATS: + request_to_validate = {'enabled': valid_enabled} + self.create_idp_validator.validate(request_to_validate) + self.update_idp_validator.validate(request_to_validate) + + def test_validate_idp_request_with_invalid_enabled_fails(self): + """Exception is raised when `enabled` isn't a boolean-like value.""" + for invalid_enabled in _INVALID_ENABLED_FORMATS: + request_to_validate = {'enabled': invalid_enabled} + self.assertRaises(exception.SchemaValidationError, + self.create_idp_validator.validate, + request_to_validate) + + self.assertRaises(exception.SchemaValidationError, + self.update_idp_validator.validate, + request_to_validate) + + def test_validate_idp_request_no_parameters(self): + """Test that schema validation with empty request body.""" + request_to_validate = {} + self.create_idp_validator.validate(request_to_validate) + + # Exception raised when no property on IdP update. + self.assertRaises(exception.SchemaValidationError, + self.update_idp_validator.validate, + request_to_validate) + + def test_validate_idp_request_with_invalid_description_fails(self): + """Exception is raised when `description` as a non-string value.""" + request_to_validate = {'description': False} + self.assertRaises(exception.SchemaValidationError, + self.create_idp_validator.validate, + request_to_validate) + + self.assertRaises(exception.SchemaValidationError, + self.update_idp_validator.validate, + request_to_validate) + + def test_validate_idp_request_with_invalid_remote_id_fails(self): + """Exception is raised when `remote_ids` is not a array.""" + request_to_validate = {"remote_ids": uuid.uuid4().hex} + self.assertRaises(exception.SchemaValidationError, + self.create_idp_validator.validate, + request_to_validate) + + self.assertRaises(exception.SchemaValidationError, + self.update_idp_validator.validate, + request_to_validate) + + def test_validate_idp_request_with_duplicated_remote_id(self): + """Exception is raised when the duplicated `remote_ids` is found.""" + idp_id = uuid.uuid4().hex + request_to_validate = {"remote_ids": [idp_id, idp_id]} + self.assertRaises(exception.SchemaValidationError, + self.create_idp_validator.validate, + request_to_validate) + + self.assertRaises(exception.SchemaValidationError, + self.update_idp_validator.validate, + request_to_validate) + + def test_validate_idp_request_remote_id_nullable(self): + """Test that `remote_ids` could be explicitly set to None""" + request_to_validate = {'remote_ids': None} + self.create_idp_validator.validate(request_to_validate) + self.update_idp_validator.validate(request_to_validate) + + +class FederationProtocolValidationTestCase(unit.BaseTestCase): + """Test for V3 Federation Protocol API validation.""" + + def setUp(self): + super(FederationProtocolValidationTestCase, self).setUp() + + schema = federation_schema.federation_protocol_schema + # create protocol and update protocol have the same shema definition, + # combine them together, no need to validate separately. + self.protocol_validator = validators.SchemaValidator(schema) + + def test_validate_protocol_request_succeeds(self): + """Test that we validate a protocol request successfully.""" + request_to_validate = {'mapping_id': uuid.uuid4().hex} + self.protocol_validator.validate(request_to_validate) + + def test_validate_protocol_request_succeeds_with_nonuuid_mapping_id(self): + """Test that we allow underscore in mapping_id value.""" + request_to_validate = {'mapping_id': 'my_mapping_id'} + self.protocol_validator.validate(request_to_validate) + + def test_validate_protocol_request_fails_with_invalid_params(self): + """Exception raised when unknown parameter is found.""" + request_to_validate = {'bogus': uuid.uuid4().hex} + self.assertRaises(exception.SchemaValidationError, + self.protocol_validator.validate, + request_to_validate) + + def test_validate_protocol_request_no_parameters(self): + """Test that schema validation with empty request body.""" + request_to_validate = {} + # 'mapping_id' is required. + self.assertRaises(exception.SchemaValidationError, + self.protocol_validator.validate, + request_to_validate) + + def test_validate_protocol_request_fails_with_invalid_mapping_id(self): + """Exception raised when mapping_id is not string.""" + request_to_validate = {'mapping_id': 12334} + self.assertRaises(exception.SchemaValidationError, + self.protocol_validator.validate, + request_to_validate) + + +class OAuth1ValidationTestCase(unit.BaseTestCase): + """Test for V3 Identity OAuth1 API validation.""" + + def setUp(self): + super(OAuth1ValidationTestCase, self).setUp() + + create = oauth1_schema.consumer_create + update = oauth1_schema.consumer_update + self.create_consumer_validator = validators.SchemaValidator(create) + self.update_consumer_validator = validators.SchemaValidator(update) + + def test_validate_consumer_request_succeeds(self): + """Test that we validate a consumer request successfully.""" + request_to_validate = {'description': uuid.uuid4().hex, + 'name': uuid.uuid4().hex} + self.create_consumer_validator.validate(request_to_validate) + self.update_consumer_validator.validate(request_to_validate) + + def test_validate_consumer_request_with_no_parameters(self): + """Test that schema validation with empty request body.""" + request_to_validate = {} + self.create_consumer_validator.validate(request_to_validate) + # At least one property should be given. + self.assertRaises(exception.SchemaValidationError, + self.update_consumer_validator.validate, + request_to_validate) + + def test_validate_consumer_request_with_invalid_description_fails(self): + """Exception is raised when `description` as a non-string value.""" + for invalid_desc in _INVALID_DESC_FORMATS: + request_to_validate = {'description': invalid_desc} + self.assertRaises(exception.SchemaValidationError, + self.create_consumer_validator.validate, + request_to_validate) + + self.assertRaises(exception.SchemaValidationError, + self.update_consumer_validator.validate, + request_to_validate) + + def test_validate_update_consumer_request_fails_with_secret(self): + """Exception raised when secret is given.""" + request_to_validate = {'secret': uuid.uuid4().hex} + self.assertRaises(exception.SchemaValidationError, + self.update_consumer_validator.validate, + request_to_validate) + + def test_validate_consumer_request_with_none_desc(self): + """Test that schema validation with None desc.""" + request_to_validate = {'description': None} + self.create_consumer_validator.validate(request_to_validate) + self.update_consumer_validator.validate(request_to_validate) diff --git a/keystone-moon/keystone/tests/unit/test_versions.py b/keystone-moon/keystone/tests/unit/test_versions.py index 40814588..2f5c2b17 100644 --- a/keystone-moon/keystone/tests/unit/test_versions.py +++ b/keystone-moon/keystone/tests/unit/test_versions.py @@ -25,9 +25,9 @@ from testtools import matchers as tt_matchers import webob from keystone.common import json_home -from keystone import controllers from keystone.tests import unit from keystone.tests.unit import utils +from keystone.version import controllers CONF = cfg.CONF @@ -74,9 +74,9 @@ v3_MEDIA_TYPES = [ ] v3_EXPECTED_RESPONSE = { - "id": "v3.4", + "id": "v3.6", "status": "stable", - "updated": "2015-03-30T00:00:00Z", + "updated": "2016-04-04T00:00:00Z", "links": [ { "rel": "self", @@ -131,6 +131,10 @@ _build_ep_filter_rel = functools.partial( json_home.build_v3_extension_resource_relation, extension_name='OS-EP-FILTER', extension_version='1.0') +_build_os_inherit_rel = functools.partial( + json_home.build_v3_extension_resource_relation, + extension_name='OS-INHERIT', extension_version='1.0') + TRUST_ID_PARAMETER_RELATION = json_home.build_v3_extension_parameter_relation( 'OS-TRUST', '1.0', 'trust_id') @@ -169,13 +173,12 @@ BASE_EP_FILTER = BASE_EP_FILTER_PREFIX + '/endpoint_groups/{endpoint_group_id}' BASE_ACCESS_TOKEN = ( '/users/{user_id}/OS-OAUTH1/access_tokens/{access_token_id}') -# TODO(stevemar): Use BASE_IDP_PROTOCOL when bug 1420125 is resolved. -FEDERATED_AUTH_URL = ('/OS-FEDERATION/identity_providers/{identity_provider}' - '/protocols/{protocol}/auth') +FEDERATED_AUTH_URL = ('/OS-FEDERATION/identity_providers/{idp_id}' + '/protocols/{protocol_id}/auth') FEDERATED_IDP_SPECIFIC_WEBSSO = ('/auth/OS-FEDERATION/identity_providers/' '{idp_id}/protocols/{protocol_id}/websso') -V3_JSON_HOME_RESOURCES_INHERIT_DISABLED = { +V3_JSON_HOME_RESOURCES = { json_home.build_v3_resource_relation('auth_tokens'): { 'href': '/auth/tokens'}, json_home.build_v3_resource_relation('auth_catalog'): { @@ -231,8 +234,8 @@ V3_JSON_HOME_RESOURCES_INHERIT_DISABLED = { _build_ec2tokens_relation(resource_name='user_credential'): { 'href-template': '/users/{user_id}/credentials/OS-EC2/{credential_id}', 'href-vars': { - 'credential_id': json_home.build_v3_extension_parameter_relation( - 'OS-EC2', '1.0', 'credential_id'), + 'credential_id': + json_home.build_v3_parameter_relation('credential_id'), 'user_id': json_home.Parameters.USER_ID, }}, _build_ec2tokens_relation(resource_name='user_credentials'): { 'href-template': '/users/{user_id}/credentials/OS-EC2', @@ -324,6 +327,22 @@ V3_JSON_HOME_RESOURCES_INHERIT_DISABLED = { 'href-template': '/roles/{role_id}', 'href-vars': { 'role_id': json_home.Parameters.ROLE_ID, }}, + json_home.build_v3_resource_relation('implied_roles'): { + 'href-template': '/roles/{prior_role_id}/implies', + 'href-vars': { + 'prior_role_id': json_home.Parameters.ROLE_ID}, + 'hints': {'status': 'experimental'}}, + json_home.build_v3_resource_relation('implied_role'): { + 'href-template': + '/roles/{prior_role_id}/implies/{implied_role_id}', + 'href-vars': { + 'prior_role_id': json_home.Parameters.ROLE_ID, + 'implied_role_id': json_home.Parameters.ROLE_ID, + }, + 'hints': {'status': 'experimental'}}, + json_home.build_v3_resource_relation('role_inferences'): { + 'href': '/role_inferences', + 'hints': {'status': 'experimental'}}, json_home.build_v3_resource_relation('role_assignments'): { 'href': '/role_assignments'}, json_home.build_v3_resource_relation('roles'): {'href': '/roles'}, @@ -394,12 +413,11 @@ V3_JSON_HOME_RESOURCES_INHERIT_DISABLED = { 'href-template': BASE_IDP_PROTOCOL, 'href-vars': { 'idp_id': IDP_ID_PARAMETER_RELATION}}, - # TODO(stevemar): Update href-vars when bug 1420125 is resolved. _build_federation_rel(resource_name='identity_provider_protocol_auth'): { 'href-template': FEDERATED_AUTH_URL, 'href-vars': { - 'identity_provider': IDP_ID_PARAMETER_RELATION, - 'protocol': PROTOCOL_ID_PARAM_RELATION, }}, + 'idp_id': IDP_ID_PARAMETER_RELATION, + 'protocol_id': PROTOCOL_ID_PARAM_RELATION, }}, _build_oauth1_rel(resource_name='access_tokens'): { 'href': '/OS-OAUTH1/access_token'}, _build_oauth1_rel(resource_name='request_tokens'): { @@ -509,6 +527,58 @@ V3_JSON_HOME_RESOURCES_INHERIT_DISABLED = { 'href-template': BASE_EP_FILTER + '/projects', 'href-vars': {'endpoint_group_id': ENDPOINT_GROUP_ID_PARAMETER_RELATION, }}, + _build_os_inherit_rel( + resource_name='domain_user_role_inherited_to_projects'): + { + 'href-template': '/OS-INHERIT/domains/{domain_id}/users/' + '{user_id}/roles/{role_id}/inherited_to_projects', + 'href-vars': { + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'role_id': json_home.Parameters.ROLE_ID, + 'user_id': json_home.Parameters.USER_ID, }}, + _build_os_inherit_rel( + resource_name='domain_group_role_inherited_to_projects'): + { + 'href-template': '/OS-INHERIT/domains/{domain_id}/groups/' + '{group_id}/roles/{role_id}/inherited_to_projects', + 'href-vars': { + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'group_id': json_home.Parameters.GROUP_ID, + 'role_id': json_home.Parameters.ROLE_ID, }}, + _build_os_inherit_rel( + resource_name='domain_user_roles_inherited_to_projects'): + { + 'href-template': '/OS-INHERIT/domains/{domain_id}/users/' + '{user_id}/roles/inherited_to_projects', + 'href-vars': { + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'user_id': json_home.Parameters.USER_ID, }}, + _build_os_inherit_rel( + resource_name='domain_group_roles_inherited_to_projects'): + { + 'href-template': '/OS-INHERIT/domains/{domain_id}/groups/' + '{group_id}/roles/inherited_to_projects', + 'href-vars': { + 'domain_id': json_home.Parameters.DOMAIN_ID, + 'group_id': json_home.Parameters.GROUP_ID, }}, + _build_os_inherit_rel( + resource_name='project_user_role_inherited_to_projects'): + { + 'href-template': '/OS-INHERIT/projects/{project_id}/users/' + '{user_id}/roles/{role_id}/inherited_to_projects', + 'href-vars': { + 'project_id': json_home.Parameters.PROJECT_ID, + 'role_id': json_home.Parameters.ROLE_ID, + 'user_id': json_home.Parameters.USER_ID, }}, + _build_os_inherit_rel( + resource_name='project_group_role_inherited_to_projects'): + { + 'href-template': '/OS-INHERIT/projects/{project_id}/groups/' + '{group_id}/roles/{role_id}/inherited_to_projects', + 'href-vars': { + 'project_id': json_home.Parameters.PROJECT_ID, + 'group_id': json_home.Parameters.GROUP_ID, + 'role_id': json_home.Parameters.ROLE_ID, }}, json_home.build_v3_resource_relation('domain_config'): { 'href-template': '/domains/{domain_id}/config', @@ -530,99 +600,23 @@ V3_JSON_HOME_RESOURCES_INHERIT_DISABLED = { 'group': json_home.build_v3_parameter_relation('config_group'), 'option': json_home.build_v3_parameter_relation('config_option')}, 'hints': {'status': 'experimental'}}, + json_home.build_v3_resource_relation('domain_config_default'): { + 'href': '/domains/config/default', + 'hints': {'status': 'experimental'}}, + json_home.build_v3_resource_relation('domain_config_default_group'): { + 'href-template': '/domains/config/{group}/default', + 'href-vars': { + 'group': json_home.build_v3_parameter_relation('config_group')}, + 'hints': {'status': 'experimental'}}, + json_home.build_v3_resource_relation('domain_config_default_option'): { + 'href-template': '/domains/config/{group}/{option}/default', + 'href-vars': { + 'group': json_home.build_v3_parameter_relation('config_group'), + 'option': json_home.build_v3_parameter_relation('config_option')}, + 'hints': {'status': 'experimental'}}, } -# with os-inherit enabled, there's some more resources. - -build_os_inherit_relation = functools.partial( - json_home.build_v3_extension_resource_relation, - extension_name='OS-INHERIT', extension_version='1.0') - -V3_JSON_HOME_RESOURCES_INHERIT_ENABLED = dict( - V3_JSON_HOME_RESOURCES_INHERIT_DISABLED) -V3_JSON_HOME_RESOURCES_INHERIT_ENABLED.update( - ( - ( - build_os_inherit_relation( - resource_name='domain_user_role_inherited_to_projects'), - { - 'href-template': '/OS-INHERIT/domains/{domain_id}/users/' - '{user_id}/roles/{role_id}/inherited_to_projects', - 'href-vars': { - 'domain_id': json_home.Parameters.DOMAIN_ID, - 'role_id': json_home.Parameters.ROLE_ID, - 'user_id': json_home.Parameters.USER_ID, - }, - } - ), - ( - build_os_inherit_relation( - resource_name='domain_group_role_inherited_to_projects'), - { - 'href-template': '/OS-INHERIT/domains/{domain_id}/groups/' - '{group_id}/roles/{role_id}/inherited_to_projects', - 'href-vars': { - 'domain_id': json_home.Parameters.DOMAIN_ID, - 'group_id': json_home.Parameters.GROUP_ID, - 'role_id': json_home.Parameters.ROLE_ID, - }, - } - ), - ( - build_os_inherit_relation( - resource_name='domain_user_roles_inherited_to_projects'), - { - 'href-template': '/OS-INHERIT/domains/{domain_id}/users/' - '{user_id}/roles/inherited_to_projects', - 'href-vars': { - 'domain_id': json_home.Parameters.DOMAIN_ID, - 'user_id': json_home.Parameters.USER_ID, - }, - } - ), - ( - build_os_inherit_relation( - resource_name='domain_group_roles_inherited_to_projects'), - { - 'href-template': '/OS-INHERIT/domains/{domain_id}/groups/' - '{group_id}/roles/inherited_to_projects', - 'href-vars': { - 'domain_id': json_home.Parameters.DOMAIN_ID, - 'group_id': json_home.Parameters.GROUP_ID, - }, - } - ), - ( - build_os_inherit_relation( - resource_name='project_user_role_inherited_to_projects'), - { - 'href-template': '/OS-INHERIT/projects/{project_id}/users/' - '{user_id}/roles/{role_id}/inherited_to_projects', - 'href-vars': { - 'project_id': json_home.Parameters.PROJECT_ID, - 'role_id': json_home.Parameters.ROLE_ID, - 'user_id': json_home.Parameters.USER_ID, - }, - } - ), - ( - build_os_inherit_relation( - resource_name='project_group_role_inherited_to_projects'), - { - 'href-template': '/OS-INHERIT/projects/{project_id}/groups/' - '{group_id}/roles/{role_id}/inherited_to_projects', - 'href-vars': { - 'project_id': json_home.Parameters.PROJECT_ID, - 'group_id': json_home.Parameters.GROUP_ID, - 'role_id': json_home.Parameters.ROLE_ID, - }, - } - ), - ) -) - - class TestClient(object): def __init__(self, app=None, token=None): self.app = app @@ -751,7 +745,7 @@ class VersionTestCase(unit.TestCase): def test_public_version_v2(self): client = TestClient(self.public_app) resp = client.get('/v2.0/') - self.assertEqual(200, resp.status_int) + self.assertEqual(http_client.OK, resp.status_int) data = jsonutils.loads(resp.body) expected = v2_VERSION_RESPONSE self._paste_in_port(expected['version'], @@ -762,7 +756,7 @@ class VersionTestCase(unit.TestCase): def test_admin_version_v2(self): client = TestClient(self.admin_app) resp = client.get('/v2.0/') - self.assertEqual(200, resp.status_int) + self.assertEqual(http_client.OK, resp.status_int) data = jsonutils.loads(resp.body) expected = v2_VERSION_RESPONSE self._paste_in_port(expected['version'], @@ -775,7 +769,7 @@ class VersionTestCase(unit.TestCase): for app in (self.public_app, self.admin_app): client = TestClient(app) resp = client.get('/v2.0/') - self.assertEqual(200, resp.status_int) + self.assertEqual(http_client.OK, resp.status_int) data = jsonutils.loads(resp.body) expected = v2_VERSION_RESPONSE self._paste_in_port(expected['version'], 'http://localhost/v2.0/') @@ -784,7 +778,7 @@ class VersionTestCase(unit.TestCase): def test_public_version_v3(self): client = TestClient(self.public_app) resp = client.get('/v3/') - self.assertEqual(200, resp.status_int) + self.assertEqual(http_client.OK, resp.status_int) data = jsonutils.loads(resp.body) expected = v3_VERSION_RESPONSE self._paste_in_port(expected['version'], @@ -796,7 +790,7 @@ class VersionTestCase(unit.TestCase): def test_admin_version_v3(self): client = TestClient(self.admin_app) resp = client.get('/v3/') - self.assertEqual(200, resp.status_int) + self.assertEqual(http_client.OK, resp.status_int) data = jsonutils.loads(resp.body) expected = v3_VERSION_RESPONSE self._paste_in_port(expected['version'], @@ -809,7 +803,7 @@ class VersionTestCase(unit.TestCase): for app in (self.public_app, self.admin_app): client = TestClient(app) resp = client.get('/v3/') - self.assertEqual(200, resp.status_int) + self.assertEqual(http_client.OK, resp.status_int) data = jsonutils.loads(resp.body) expected = v3_VERSION_RESPONSE self._paste_in_port(expected['version'], 'http://localhost/v3/') @@ -824,7 +818,7 @@ class VersionTestCase(unit.TestCase): # request to /v3 should pass resp = client.get('/v3/') - self.assertEqual(200, resp.status_int) + self.assertEqual(http_client.OK, resp.status_int) data = jsonutils.loads(resp.body) expected = v3_VERSION_RESPONSE self._paste_in_port(expected['version'], @@ -857,7 +851,7 @@ class VersionTestCase(unit.TestCase): # request to /v2.0 should pass resp = client.get('/v2.0/') - self.assertEqual(200, resp.status_int) + self.assertEqual(http_client.OK, resp.status_int) data = jsonutils.loads(resp.body) expected = v2_VERSION_RESPONSE self._paste_in_port(expected['version'], @@ -897,7 +891,7 @@ class VersionTestCase(unit.TestCase): # then the server responds with a JSON Home document. exp_json_home_data = { - 'resources': V3_JSON_HOME_RESOURCES_INHERIT_DISABLED} + 'resources': V3_JSON_HOME_RESOURCES} self._test_json_home('/v3', exp_json_home_data) @@ -906,7 +900,7 @@ class VersionTestCase(unit.TestCase): # then the server responds with a JSON Home document. exp_json_home_data = copy.deepcopy({ - 'resources': V3_JSON_HOME_RESOURCES_INHERIT_DISABLED}) + 'resources': V3_JSON_HOME_RESOURCES}) json_home.translate_urls(exp_json_home_data, '/v3') self._test_json_home('/', exp_json_home_data) @@ -1022,45 +1016,6 @@ class VersionSingleAppTestCase(unit.TestCase): self._test_version('admin') -class VersionInheritEnabledTestCase(unit.TestCase): - def setUp(self): - super(VersionInheritEnabledTestCase, self).setUp() - self.load_backends() - self.public_app = self.loadapp('keystone', 'main') - self.admin_app = self.loadapp('keystone', 'admin') - - self.config_fixture.config( - public_endpoint='http://localhost:%(public_port)d', - admin_endpoint='http://localhost:%(admin_port)d') - - def config_overrides(self): - super(VersionInheritEnabledTestCase, self).config_overrides() - admin_port = random.randint(10000, 30000) - public_port = random.randint(40000, 60000) - self.config_fixture.config(group='eventlet_server', - public_port=public_port, - admin_port=admin_port) - - self.config_fixture.config(group='os_inherit', enabled=True) - - def test_json_home_v3(self): - # If the request is /v3 and the Accept header is application/json-home - # then the server responds with a JSON Home document. - - client = TestClient(self.public_app) - resp = client.get('/v3/', headers={'Accept': 'application/json-home'}) - - self.assertThat(resp.status, tt_matchers.Equals('200 OK')) - self.assertThat(resp.headers['Content-Type'], - tt_matchers.Equals('application/json-home')) - - exp_json_home_data = { - 'resources': V3_JSON_HOME_RESOURCES_INHERIT_ENABLED} - - self.assertThat(jsonutils.loads(resp.body), - tt_matchers.Equals(exp_json_home_data)) - - class VersionBehindSslTestCase(unit.TestCase): def setUp(self): super(VersionBehindSslTestCase, self).setUp() diff --git a/keystone-moon/keystone/tests/unit/test_wsgi.py b/keystone-moon/keystone/tests/unit/test_wsgi.py index ed4c67d6..564d7406 100644 --- a/keystone-moon/keystone/tests/unit/test_wsgi.py +++ b/keystone-moon/keystone/tests/unit/test_wsgi.py @@ -85,7 +85,7 @@ class ApplicationTest(BaseWSGITest): def test_response_content_type(self): req = self._make_request() resp = req.get_response(self.app) - self.assertEqual(resp.content_type, 'application/json') + self.assertEqual('application/json', resp.content_type) def test_query_string_available(self): class FakeApp(wsgi.Application): @@ -93,7 +93,7 @@ class ApplicationTest(BaseWSGITest): return context['query_string'] req = self._make_request(url='/?1=2') resp = req.get_response(FakeApp()) - self.assertEqual(jsonutils.loads(resp.body), {'1': '2'}) + self.assertEqual({'1': '2'}, jsonutils.loads(resp.body)) def test_headers_available(self): class FakeApp(wsgi.Application): @@ -112,15 +112,16 @@ class ApplicationTest(BaseWSGITest): resp = wsgi.render_response(body=data) self.assertEqual('200 OK', resp.status) - self.assertEqual(200, resp.status_int) + self.assertEqual(http_client.OK, resp.status_int) self.assertEqual(body, resp.body) self.assertEqual('X-Auth-Token', resp.headers.get('Vary')) self.assertEqual(str(len(body)), resp.headers.get('Content-Length')) def test_render_response_custom_status(self): - resp = wsgi.render_response(status=(501, 'Not Implemented')) + resp = wsgi.render_response( + status=(http_client.NOT_IMPLEMENTED, 'Not Implemented')) self.assertEqual('501 Not Implemented', resp.status) - self.assertEqual(501, resp.status_int) + self.assertEqual(http_client.NOT_IMPLEMENTED, resp.status_int) def test_successful_require_attribute(self): app = FakeAttributeCheckerApp() @@ -169,19 +170,31 @@ class ApplicationTest(BaseWSGITest): self.assertEqual('Some-Value', resp.headers.get('Custom-Header')) self.assertEqual('X-Auth-Token', resp.headers.get('Vary')) + def test_render_response_non_str_headers_converted(self): + resp = wsgi.render_response( + headers=[('Byte-Header', 'Byte-Value'), + (u'Unicode-Header', u'Unicode-Value')]) + # assert that all headers are identified. + self.assertThat(resp.headers, matchers.HasLength(4)) + self.assertEqual('Unicode-Value', resp.headers.get('Unicode-Header')) + # assert that unicode value is converted, the expected type is str + # on both python2 and python3. + self.assertEqual(str, + type(resp.headers.get('Unicode-Header'))) + def test_render_response_no_body(self): resp = wsgi.render_response() self.assertEqual('204 No Content', resp.status) - self.assertEqual(204, resp.status_int) + self.assertEqual(http_client.NO_CONTENT, resp.status_int) self.assertEqual(b'', resp.body) self.assertEqual('0', resp.headers.get('Content-Length')) self.assertIsNone(resp.headers.get('Content-Type')) def test_render_response_head_with_body(self): resp = wsgi.render_response({'id': uuid.uuid4().hex}, method='HEAD') - self.assertEqual(200, resp.status_int) + self.assertEqual(http_client.OK, resp.status_int) self.assertEqual(b'', resp.body) - self.assertNotEqual(resp.headers.get('Content-Length'), '0') + self.assertNotEqual('0', resp.headers.get('Content-Length')) self.assertEqual('application/json', resp.headers.get('Content-Type')) def test_application_local_config(self): @@ -200,7 +213,9 @@ class ApplicationTest(BaseWSGITest): def test_render_exception_host(self): e = exception.Unauthorized(message=u'\u7f51\u7edc') - context = {'host_url': 'http://%s:5000' % uuid.uuid4().hex} + req = self._make_request(url='/') + context = {'host_url': 'http://%s:5000' % uuid.uuid4().hex, + 'environment': req.environ} resp = wsgi.render_exception(e, context=context) self.assertEqual(http_client.UNAUTHORIZED, resp.status_int) @@ -225,6 +240,77 @@ class ApplicationTest(BaseWSGITest): self.assertEqual({'name': u'nonexit\xe8nt'}, jsonutils.loads(resp.body)) + def test_base_url(self): + class FakeApp(wsgi.Application): + def index(self, context): + return self.base_url(context, 'public') + req = self._make_request(url='/') + # NOTE(gyee): according to wsgiref, if HTTP_HOST is present in the + # request environment, it will be used to construct the base url. + # SERVER_NAME and SERVER_PORT will be ignored. These are standard + # WSGI environment variables populated by the webserver. + req.environ.update({ + 'SCRIPT_NAME': '/identity', + 'SERVER_NAME': '1.2.3.4', + 'wsgi.url_scheme': 'http', + 'SERVER_PORT': '80', + 'HTTP_HOST': '1.2.3.4', + }) + resp = req.get_response(FakeApp()) + self.assertEqual(b"http://1.2.3.4/identity", resp.body) + + # if HTTP_HOST is absent, SERVER_NAME and SERVER_PORT will be used + req = self._make_request(url='/') + del req.environ['HTTP_HOST'] + req.environ.update({ + 'SCRIPT_NAME': '/identity', + 'SERVER_NAME': '1.1.1.1', + 'wsgi.url_scheme': 'http', + 'SERVER_PORT': '1234', + }) + resp = req.get_response(FakeApp()) + self.assertEqual(b"http://1.1.1.1:1234/identity", resp.body) + + # make sure keystone normalize the standard HTTP port 80 by stripping + # it + req = self._make_request(url='/') + req.environ.update({'HTTP_HOST': 'foo:80', + 'SCRIPT_NAME': '/identity'}) + resp = req.get_response(FakeApp()) + self.assertEqual(b"http://foo/identity", resp.body) + + # make sure keystone normalize the standard HTTPS port 443 by stripping + # it + req = self._make_request(url='/') + req.environ.update({'HTTP_HOST': 'foo:443', + 'SCRIPT_NAME': '/identity', + 'wsgi.url_scheme': 'https'}) + resp = req.get_response(FakeApp()) + self.assertEqual(b"https://foo/identity", resp.body) + + # make sure non-standard port is preserved + req = self._make_request(url='/') + req.environ.update({'HTTP_HOST': 'foo:1234', + 'SCRIPT_NAME': '/identity'}) + resp = req.get_response(FakeApp()) + self.assertEqual(b"http://foo:1234/identity", resp.body) + + # make sure version portion of the SCRIPT_NAME, '/v2.0', is stripped + # from base url + req = self._make_request(url='/') + req.environ.update({'HTTP_HOST': 'foo:80', + 'SCRIPT_NAME': '/bar/identity/v2.0'}) + resp = req.get_response(FakeApp()) + self.assertEqual(b"http://foo/bar/identity", resp.body) + + # make sure version portion of the SCRIPT_NAME, '/v3' is stripped from + # base url + req = self._make_request(url='/') + req.environ.update({'HTTP_HOST': 'foo:80', + 'SCRIPT_NAME': '/identity/v3'}) + resp = req.get_response(FakeApp()) + self.assertEqual(b"http://foo/identity", resp.body) + class ExtensionRouterTest(BaseWSGITest): def test_extensionrouter_local_config(self): @@ -293,24 +379,15 @@ class MiddlewareTest(BaseWSGITest): self.assertEqual(exception.UnexpectedError.code, resp.status_int) return resp - # Exception data should not be in the message when debug is False - self.config_fixture.config(debug=False) + # Exception data should not be in the message when insecure_debug is + # False + self.config_fixture.config(debug=False, insecure_debug=False) self.assertNotIn(exception_str, do_request().body) - # Exception data should be in the message when debug is True - self.config_fixture.config(debug=True) + # Exception data should be in the message when insecure_debug is True + self.config_fixture.config(debug=True, insecure_debug=True) self.assertIn(exception_str, do_request().body) - def test_middleware_local_config(self): - class FakeMiddleware(wsgi.Middleware): - def __init__(self, *args, **kwargs): - self.kwargs = kwargs - - factory = FakeMiddleware.factory({}, testkey="test") - app = factory(self.app) - self.assertIn("testkey", app.kwargs) - self.assertEqual("test", app.kwargs["testkey"]) - class LocalizedResponseTest(unit.TestCase): def test_request_match_default(self): @@ -345,8 +422,8 @@ class LocalizedResponseTest(unit.TestCase): def test_static_translated_string_is_lazy_translatable(self): # Statically created message strings are an object that can get # lazy-translated rather than a regular string. - self.assertNotEqual(type(exception.Unauthorized.message_format), - six.text_type) + self.assertNotEqual(six.text_type, + type(exception.Unauthorized.message_format)) @mock.patch.object(oslo_i18n, 'get_available_languages') def test_get_localized_response(self, mock_gal): @@ -457,12 +534,14 @@ class ServerTest(unit.TestCase): server.start() self.addCleanup(server.stop) - self.assertEqual(2, mock_sock_dup.setsockopt.call_count) - - # Test the last set of call args i.e. for the keepidle - mock_sock_dup.setsockopt.assert_called_with(socket.IPPROTO_TCP, - socket.TCP_KEEPIDLE, - 1) + if hasattr(socket, 'TCP_KEEPIDLE'): + self.assertEqual(2, mock_sock_dup.setsockopt.call_count) + # Test the last set of call args i.e. for the keepidle + mock_sock_dup.setsockopt.assert_called_with(socket.IPPROTO_TCP, + socket.TCP_KEEPIDLE, + 1) + else: + self.assertEqual(1, mock_sock_dup.setsockopt.call_count) self.assertTrue(mock_listen.called) diff --git a/keystone-moon/keystone/tests/unit/tests/test_core.py b/keystone-moon/keystone/tests/unit/tests/test_core.py index 50f1309e..56e42bcc 100644 --- a/keystone-moon/keystone/tests/unit/tests/test_core.py +++ b/keystone-moon/keystone/tests/unit/tests/test_core.py @@ -39,7 +39,7 @@ class TestTestCase(unit.TestCase): # If the arguments are invalid for the string in a log it raises an # exception during testing. self.assertThat( - lambda: LOG.warn('String %(p1)s %(p2)s', {'p1': 'something'}), + lambda: LOG.warning('String %(p1)s %(p2)s', {'p1': 'something'}), matchers.raises(KeyError)) def test_sa_warning(self): diff --git a/keystone-moon/keystone/tests/unit/token/test_backends.py b/keystone-moon/keystone/tests/unit/token/test_backends.py new file mode 100644 index 00000000..feb7e017 --- /dev/null +++ b/keystone-moon/keystone/tests/unit/token/test_backends.py @@ -0,0 +1,551 @@ +# 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 copy +import datetime +import hashlib +import uuid + +from keystoneclient.common import cms +from oslo_config import cfg +from oslo_utils import timeutils +import six +from six.moves import range + +from keystone import exception +from keystone.tests import unit +from keystone.tests.unit import utils as test_utils +from keystone.token import provider + + +CONF = cfg.CONF +NULL_OBJECT = object() + + +class TokenTests(object): + def _create_token_id(self): + # Use a token signed by the cms module + token_id = "" + for i in range(1, 20): + token_id += uuid.uuid4().hex + return cms.cms_sign_token(token_id, + CONF.signing.certfile, + CONF.signing.keyfile) + + def _assert_revoked_token_list_matches_token_persistence( + self, revoked_token_id_list): + # Assert that the list passed in matches the list returned by the + # token persistence service + persistence_list = [ + x['id'] + for x in self.token_provider_api.list_revoked_tokens() + ] + self.assertEqual(persistence_list, revoked_token_id_list) + + def test_token_crud(self): + token_id = self._create_token_id() + data = {'id': token_id, 'a': 'b', + 'trust_id': None, + 'user': {'id': 'testuserid'}, + 'token_data': {'access': {'token': { + 'audit_ids': [uuid.uuid4().hex]}}}} + data_ref = self.token_provider_api._persistence.create_token(token_id, + data) + expires = data_ref.pop('expires') + data_ref.pop('user_id') + self.assertIsInstance(expires, datetime.datetime) + data_ref.pop('id') + data.pop('id') + self.assertDictEqual(data, data_ref) + + new_data_ref = self.token_provider_api._persistence.get_token(token_id) + expires = new_data_ref.pop('expires') + self.assertIsInstance(expires, datetime.datetime) + new_data_ref.pop('user_id') + new_data_ref.pop('id') + + self.assertEqual(data, new_data_ref) + + self.token_provider_api._persistence.delete_token(token_id) + self.assertRaises( + exception.TokenNotFound, + self.token_provider_api._persistence.get_token, token_id) + self.assertRaises( + exception.TokenNotFound, + self.token_provider_api._persistence.delete_token, token_id) + + def create_token_sample_data(self, token_id=None, tenant_id=None, + trust_id=None, user_id=None, expires=None): + if token_id is None: + token_id = self._create_token_id() + if user_id is None: + user_id = 'testuserid' + # FIXME(morganfainberg): These tokens look nothing like "Real" tokens. + # This should be fixed when token issuance is cleaned up. + data = {'id': token_id, 'a': 'b', + 'user': {'id': user_id}, + 'access': {'token': {'audit_ids': [uuid.uuid4().hex]}}} + if tenant_id is not None: + data['tenant'] = {'id': tenant_id, 'name': tenant_id} + if tenant_id is NULL_OBJECT: + data['tenant'] = None + if expires is not None: + data['expires'] = expires + if trust_id is not None: + data['trust_id'] = trust_id + data['access'].setdefault('trust', {}) + # Testuserid2 is used here since a trustee will be different in + # the cases of impersonation and therefore should not match the + # token's user_id. + data['access']['trust']['trustee_user_id'] = 'testuserid2' + data['token_version'] = provider.V2 + # Issue token stores a copy of all token data at token['token_data']. + # This emulates that assumption as part of the test. + data['token_data'] = copy.deepcopy(data) + new_token = self.token_provider_api._persistence.create_token(token_id, + data) + return new_token['id'], data + + def test_delete_tokens(self): + tokens = self.token_provider_api._persistence._list_tokens( + 'testuserid') + self.assertEqual(0, len(tokens)) + token_id1, data = self.create_token_sample_data( + tenant_id='testtenantid') + token_id2, data = self.create_token_sample_data( + tenant_id='testtenantid') + token_id3, data = self.create_token_sample_data( + tenant_id='testtenantid', + user_id='testuserid1') + tokens = self.token_provider_api._persistence._list_tokens( + 'testuserid') + self.assertEqual(2, len(tokens)) + self.assertIn(token_id2, tokens) + self.assertIn(token_id1, tokens) + self.token_provider_api._persistence.delete_tokens( + user_id='testuserid', + tenant_id='testtenantid') + tokens = self.token_provider_api._persistence._list_tokens( + 'testuserid') + self.assertEqual(0, len(tokens)) + self.assertRaises(exception.TokenNotFound, + self.token_provider_api._persistence.get_token, + token_id1) + self.assertRaises(exception.TokenNotFound, + self.token_provider_api._persistence.get_token, + token_id2) + + self.token_provider_api._persistence.get_token(token_id3) + + def test_delete_tokens_trust(self): + tokens = self.token_provider_api._persistence._list_tokens( + user_id='testuserid') + self.assertEqual(0, len(tokens)) + token_id1, data = self.create_token_sample_data( + tenant_id='testtenantid', + trust_id='testtrustid') + token_id2, data = self.create_token_sample_data( + tenant_id='testtenantid', + user_id='testuserid1', + trust_id='testtrustid1') + tokens = self.token_provider_api._persistence._list_tokens( + 'testuserid') + self.assertEqual(1, len(tokens)) + self.assertIn(token_id1, tokens) + self.token_provider_api._persistence.delete_tokens( + user_id='testuserid', + tenant_id='testtenantid', + trust_id='testtrustid') + self.assertRaises(exception.TokenNotFound, + self.token_provider_api._persistence.get_token, + token_id1) + self.token_provider_api._persistence.get_token(token_id2) + + def _test_token_list(self, token_list_fn): + tokens = token_list_fn('testuserid') + self.assertEqual(0, len(tokens)) + token_id1, data = self.create_token_sample_data() + tokens = token_list_fn('testuserid') + self.assertEqual(1, len(tokens)) + self.assertIn(token_id1, tokens) + token_id2, data = self.create_token_sample_data() + tokens = token_list_fn('testuserid') + self.assertEqual(2, len(tokens)) + self.assertIn(token_id2, tokens) + self.assertIn(token_id1, tokens) + self.token_provider_api._persistence.delete_token(token_id1) + tokens = token_list_fn('testuserid') + self.assertIn(token_id2, tokens) + self.assertNotIn(token_id1, tokens) + self.token_provider_api._persistence.delete_token(token_id2) + tokens = token_list_fn('testuserid') + self.assertNotIn(token_id2, tokens) + self.assertNotIn(token_id1, tokens) + + # tenant-specific tokens + tenant1 = uuid.uuid4().hex + tenant2 = uuid.uuid4().hex + token_id3, data = self.create_token_sample_data(tenant_id=tenant1) + token_id4, data = self.create_token_sample_data(tenant_id=tenant2) + # test for existing but empty tenant (LP:1078497) + token_id5, data = self.create_token_sample_data(tenant_id=NULL_OBJECT) + tokens = token_list_fn('testuserid') + self.assertEqual(3, len(tokens)) + self.assertNotIn(token_id1, tokens) + self.assertNotIn(token_id2, tokens) + self.assertIn(token_id3, tokens) + self.assertIn(token_id4, tokens) + self.assertIn(token_id5, tokens) + tokens = token_list_fn('testuserid', tenant2) + self.assertEqual(1, len(tokens)) + self.assertNotIn(token_id1, tokens) + self.assertNotIn(token_id2, tokens) + self.assertNotIn(token_id3, tokens) + self.assertIn(token_id4, tokens) + + def test_token_list(self): + self._test_token_list( + self.token_provider_api._persistence._list_tokens) + + def test_token_list_trust(self): + trust_id = uuid.uuid4().hex + token_id5, data = self.create_token_sample_data(trust_id=trust_id) + tokens = self.token_provider_api._persistence._list_tokens( + 'testuserid', trust_id=trust_id) + self.assertEqual(1, len(tokens)) + self.assertIn(token_id5, tokens) + + def test_get_token_returns_not_found(self): + self.assertRaises(exception.TokenNotFound, + self.token_provider_api._persistence.get_token, + uuid.uuid4().hex) + + def test_delete_token_returns_not_found(self): + self.assertRaises(exception.TokenNotFound, + self.token_provider_api._persistence.delete_token, + uuid.uuid4().hex) + + def test_expired_token(self): + token_id = uuid.uuid4().hex + expire_time = timeutils.utcnow() - datetime.timedelta(minutes=1) + data = {'id_hash': token_id, 'id': token_id, 'a': 'b', + 'expires': expire_time, + 'trust_id': None, + 'user': {'id': 'testuserid'}} + data_ref = self.token_provider_api._persistence.create_token(token_id, + data) + data_ref.pop('user_id') + self.assertDictEqual(data, data_ref) + self.assertRaises(exception.TokenNotFound, + self.token_provider_api._persistence.get_token, + token_id) + + def test_null_expires_token(self): + token_id = uuid.uuid4().hex + data = {'id': token_id, 'id_hash': token_id, 'a': 'b', 'expires': None, + 'user': {'id': 'testuserid'}} + data_ref = self.token_provider_api._persistence.create_token(token_id, + data) + self.assertIsNotNone(data_ref['expires']) + new_data_ref = self.token_provider_api._persistence.get_token(token_id) + + # MySQL doesn't store microseconds, so discard them before testing + data_ref['expires'] = data_ref['expires'].replace(microsecond=0) + new_data_ref['expires'] = new_data_ref['expires'].replace( + microsecond=0) + + self.assertEqual(data_ref, new_data_ref) + + def check_list_revoked_tokens(self, token_infos): + revocation_list = self.token_provider_api.list_revoked_tokens() + revoked_ids = [x['id'] for x in revocation_list] + revoked_audit_ids = [x['audit_id'] for x in revocation_list] + self._assert_revoked_token_list_matches_token_persistence(revoked_ids) + for token_id, audit_id in token_infos: + self.assertIn(token_id, revoked_ids) + self.assertIn(audit_id, revoked_audit_ids) + + def delete_token(self): + token_id = uuid.uuid4().hex + audit_id = uuid.uuid4().hex + data = {'id_hash': token_id, 'id': token_id, 'a': 'b', + 'user': {'id': 'testuserid'}, + 'token_data': {'token': {'audit_ids': [audit_id]}}} + data_ref = self.token_provider_api._persistence.create_token(token_id, + data) + self.token_provider_api._persistence.delete_token(token_id) + self.assertRaises( + exception.TokenNotFound, + self.token_provider_api._persistence.get_token, + data_ref['id']) + self.assertRaises( + exception.TokenNotFound, + self.token_provider_api._persistence.delete_token, + data_ref['id']) + return (token_id, audit_id) + + def test_list_revoked_tokens_returns_empty_list(self): + revoked_ids = [x['id'] + for x in self.token_provider_api.list_revoked_tokens()] + self._assert_revoked_token_list_matches_token_persistence(revoked_ids) + self.assertEqual([], revoked_ids) + + def test_list_revoked_tokens_for_single_token(self): + self.check_list_revoked_tokens([self.delete_token()]) + + def test_list_revoked_tokens_for_multiple_tokens(self): + self.check_list_revoked_tokens([self.delete_token() + for x in range(2)]) + + def test_flush_expired_token(self): + token_id = uuid.uuid4().hex + expire_time = timeutils.utcnow() - datetime.timedelta(minutes=1) + data = {'id_hash': token_id, 'id': token_id, 'a': 'b', + 'expires': expire_time, + 'trust_id': None, + 'user': {'id': 'testuserid'}} + data_ref = self.token_provider_api._persistence.create_token(token_id, + data) + data_ref.pop('user_id') + self.assertDictEqual(data, data_ref) + + token_id = uuid.uuid4().hex + expire_time = timeutils.utcnow() + datetime.timedelta(minutes=1) + data = {'id_hash': token_id, 'id': token_id, 'a': 'b', + 'expires': expire_time, + 'trust_id': None, + 'user': {'id': 'testuserid'}} + data_ref = self.token_provider_api._persistence.create_token(token_id, + data) + data_ref.pop('user_id') + self.assertDictEqual(data, data_ref) + + self.token_provider_api._persistence.flush_expired_tokens() + tokens = self.token_provider_api._persistence._list_tokens( + 'testuserid') + self.assertEqual(1, len(tokens)) + self.assertIn(token_id, tokens) + + @unit.skip_if_cache_disabled('token') + def test_revocation_list_cache(self): + expire_time = timeutils.utcnow() + datetime.timedelta(minutes=10) + token_id = uuid.uuid4().hex + token_data = {'id_hash': token_id, 'id': token_id, 'a': 'b', + 'expires': expire_time, + 'trust_id': None, + 'user': {'id': 'testuserid'}, + 'token_data': {'token': { + 'audit_ids': [uuid.uuid4().hex]}}} + token2_id = uuid.uuid4().hex + token2_data = {'id_hash': token2_id, 'id': token2_id, 'a': 'b', + 'expires': expire_time, + 'trust_id': None, + 'user': {'id': 'testuserid'}, + 'token_data': {'token': { + 'audit_ids': [uuid.uuid4().hex]}}} + # Create 2 Tokens. + self.token_provider_api._persistence.create_token(token_id, + token_data) + self.token_provider_api._persistence.create_token(token2_id, + token2_data) + # Verify the revocation list is empty. + self.assertEqual( + [], self.token_provider_api._persistence.list_revoked_tokens()) + self.assertEqual([], self.token_provider_api.list_revoked_tokens()) + # Delete a token directly, bypassing the manager. + self.token_provider_api._persistence.driver.delete_token(token_id) + # Verify the revocation list is still empty. + self.assertEqual( + [], self.token_provider_api._persistence.list_revoked_tokens()) + self.assertEqual([], self.token_provider_api.list_revoked_tokens()) + # Invalidate the revocation list. + self.token_provider_api._persistence.invalidate_revocation_list() + # Verify the deleted token is in the revocation list. + revoked_ids = [x['id'] + for x in self.token_provider_api.list_revoked_tokens()] + self._assert_revoked_token_list_matches_token_persistence(revoked_ids) + self.assertIn(token_id, revoked_ids) + # Delete the second token, through the manager + self.token_provider_api._persistence.delete_token(token2_id) + revoked_ids = [x['id'] + for x in self.token_provider_api.list_revoked_tokens()] + self._assert_revoked_token_list_matches_token_persistence(revoked_ids) + # Verify both tokens are in the revocation list. + self.assertIn(token_id, revoked_ids) + self.assertIn(token2_id, revoked_ids) + + def _test_predictable_revoked_pki_token_id(self, hash_fn): + token_id = self._create_token_id() + token_id_hash = hash_fn(token_id.encode('utf-8')).hexdigest() + token = {'user': {'id': uuid.uuid4().hex}, + 'token_data': {'token': {'audit_ids': [uuid.uuid4().hex]}}} + + self.token_provider_api._persistence.create_token(token_id, token) + self.token_provider_api._persistence.delete_token(token_id) + + revoked_ids = [x['id'] + for x in self.token_provider_api.list_revoked_tokens()] + self._assert_revoked_token_list_matches_token_persistence(revoked_ids) + self.assertIn(token_id_hash, revoked_ids) + self.assertNotIn(token_id, revoked_ids) + for t in self.token_provider_api._persistence.list_revoked_tokens(): + self.assertIn('expires', t) + + def test_predictable_revoked_pki_token_id_default(self): + self._test_predictable_revoked_pki_token_id(hashlib.md5) + + def test_predictable_revoked_pki_token_id_sha256(self): + self.config_fixture.config(group='token', hash_algorithm='sha256') + self._test_predictable_revoked_pki_token_id(hashlib.sha256) + + def test_predictable_revoked_uuid_token_id(self): + token_id = uuid.uuid4().hex + token = {'user': {'id': uuid.uuid4().hex}, + 'token_data': {'token': {'audit_ids': [uuid.uuid4().hex]}}} + + self.token_provider_api._persistence.create_token(token_id, token) + self.token_provider_api._persistence.delete_token(token_id) + + revoked_tokens = self.token_provider_api.list_revoked_tokens() + revoked_ids = [x['id'] for x in revoked_tokens] + self._assert_revoked_token_list_matches_token_persistence(revoked_ids) + self.assertIn(token_id, revoked_ids) + for t in revoked_tokens: + self.assertIn('expires', t) + + def test_create_unicode_token_id(self): + token_id = six.text_type(self._create_token_id()) + self.create_token_sample_data(token_id=token_id) + self.token_provider_api._persistence.get_token(token_id) + + def test_create_unicode_user_id(self): + user_id = six.text_type(uuid.uuid4().hex) + token_id, data = self.create_token_sample_data(user_id=user_id) + self.token_provider_api._persistence.get_token(token_id) + + def test_token_expire_timezone(self): + + @test_utils.timezone + def _create_token(expire_time): + token_id = uuid.uuid4().hex + user_id = six.text_type(uuid.uuid4().hex) + return self.create_token_sample_data(token_id=token_id, + user_id=user_id, + expires=expire_time) + + for d in ['+0', '-11', '-8', '-5', '+5', '+8', '+14']: + test_utils.TZ = 'UTC' + d + expire_time = timeutils.utcnow() + datetime.timedelta(minutes=1) + token_id, data_in = _create_token(expire_time) + data_get = self.token_provider_api._persistence.get_token(token_id) + + self.assertEqual(data_in['id'], data_get['id'], + 'TZ=%s' % test_utils.TZ) + + expire_time_expired = ( + timeutils.utcnow() + datetime.timedelta(minutes=-1)) + token_id, data_in = _create_token(expire_time_expired) + self.assertRaises(exception.TokenNotFound, + self.token_provider_api._persistence.get_token, + data_in['id']) + + +class TokenCacheInvalidation(object): + def _create_test_data(self): + self.user = unit.new_user_ref( + domain_id=CONF.identity.default_domain_id) + self.tenant = unit.new_project_ref( + domain_id=CONF.identity.default_domain_id) + + # Create an equivalent of a scoped token + token_dict = {'user': self.user, 'tenant': self.tenant, + 'metadata': {}, 'id': 'placeholder'} + token_id, data = self.token_provider_api.issue_v2_token(token_dict) + self.scoped_token_id = token_id + + # ..and an un-scoped one + token_dict = {'user': self.user, 'tenant': None, + 'metadata': {}, 'id': 'placeholder'} + token_id, data = self.token_provider_api.issue_v2_token(token_dict) + self.unscoped_token_id = token_id + + # Validate them, in the various ways possible - this will load the + # responses into the token cache. + self._check_scoped_tokens_are_valid() + self._check_unscoped_tokens_are_valid() + + def _check_unscoped_tokens_are_invalid(self): + self.assertRaises( + exception.TokenNotFound, + self.token_provider_api.validate_token, + self.unscoped_token_id) + self.assertRaises( + exception.TokenNotFound, + self.token_provider_api.validate_v2_token, + self.unscoped_token_id) + + def _check_scoped_tokens_are_invalid(self): + self.assertRaises( + exception.TokenNotFound, + self.token_provider_api.validate_token, + self.scoped_token_id) + self.assertRaises( + exception.TokenNotFound, + self.token_provider_api.validate_token, + self.scoped_token_id, + self.tenant['id']) + self.assertRaises( + exception.TokenNotFound, + self.token_provider_api.validate_v2_token, + self.scoped_token_id) + self.assertRaises( + exception.TokenNotFound, + self.token_provider_api.validate_v2_token, + self.scoped_token_id, + self.tenant['id']) + + def _check_scoped_tokens_are_valid(self): + self.token_provider_api.validate_token(self.scoped_token_id) + self.token_provider_api.validate_token( + self.scoped_token_id, belongs_to=self.tenant['id']) + self.token_provider_api.validate_v2_token(self.scoped_token_id) + self.token_provider_api.validate_v2_token( + self.scoped_token_id, belongs_to=self.tenant['id']) + + def _check_unscoped_tokens_are_valid(self): + self.token_provider_api.validate_token(self.unscoped_token_id) + self.token_provider_api.validate_v2_token(self.unscoped_token_id) + + def test_delete_unscoped_token(self): + self.token_provider_api._persistence.delete_token( + self.unscoped_token_id) + self._check_unscoped_tokens_are_invalid() + self._check_scoped_tokens_are_valid() + + def test_delete_scoped_token_by_id(self): + self.token_provider_api._persistence.delete_token(self.scoped_token_id) + self._check_scoped_tokens_are_invalid() + self._check_unscoped_tokens_are_valid() + + def test_delete_scoped_token_by_user(self): + self.token_provider_api._persistence.delete_tokens(self.user['id']) + # Since we are deleting all tokens for this user, they should all + # now be invalid. + self._check_scoped_tokens_are_invalid() + self._check_unscoped_tokens_are_invalid() + + def test_delete_scoped_token_by_user_and_tenant(self): + self.token_provider_api._persistence.delete_tokens( + self.user['id'], + tenant_id=self.tenant['id']) + self._check_scoped_tokens_are_invalid() + self._check_unscoped_tokens_are_valid() diff --git a/keystone-moon/keystone/tests/unit/token/test_fernet_provider.py b/keystone-moon/keystone/tests/unit/token/test_fernet_provider.py index bfb590db..5f51d7b3 100644 --- a/keystone-moon/keystone/tests/unit/token/test_fernet_provider.py +++ b/keystone-moon/keystone/tests/unit/token/test_fernet_provider.py @@ -22,8 +22,8 @@ from six.moves import urllib from keystone.common import config from keystone.common import utils -from keystone.contrib.federation import constants as federation_constants from keystone import exception +from keystone.federation import constants as federation_constants from keystone.tests import unit from keystone.tests.unit import ksfixtures from keystone.tests.unit.ksfixtures import database @@ -48,17 +48,25 @@ class TestFernetTokenProvider(unit.TestCase): def test_needs_persistence_returns_false(self): self.assertFalse(self.provider.needs_persistence()) - def test_invalid_v3_token_raises_404(self): - self.assertRaises( + def test_invalid_v3_token_raises_token_not_found(self): + # NOTE(lbragstad): Here we use the validate_non_persistent_token() + # methods because the validate_v3_token() method is strictly for + # validating UUID formatted tokens. It is written to assume cached + # tokens from a backend, where validate_non_persistent_token() is not. + token_id = uuid.uuid4().hex + e = self.assertRaises( exception.TokenNotFound, - self.provider.validate_v3_token, - uuid.uuid4().hex) + self.provider.validate_non_persistent_token, + token_id) + self.assertIn(token_id, u'%s' % e) - def test_invalid_v2_token_raises_404(self): - self.assertRaises( + def test_invalid_v2_token_raises_token_not_found(self): + token_id = uuid.uuid4().hex + e = self.assertRaises( exception.TokenNotFound, - self.provider.validate_v2_token, - uuid.uuid4().hex) + self.provider.validate_non_persistent_token, + token_id) + self.assertIn(token_id, u'%s' % e) class TestValidate(unit.TestCase): @@ -91,7 +99,6 @@ class TestValidate(unit.TestCase): token = token_data['token'] self.assertIsInstance(token['audit_ids'], list) self.assertIsInstance(token['expires_at'], str) - self.assertEqual({}, token['extras']) self.assertIsInstance(token['issued_at'], str) self.assertEqual(method_names, token['methods']) exp_user_info = { @@ -200,7 +207,7 @@ class TestValidate(unit.TestCase): def test_validate_v3_token_validation_error_exc(self): # When the token format isn't recognized, TokenNotFound is raised. - # A uuid string isn't a valid fernet token. + # A uuid string isn't a valid Fernet token. token_id = uuid.uuid4().hex self.assertRaises(exception.TokenNotFound, self.token_provider_api.validate_v3_token, token_id) @@ -214,10 +221,14 @@ class TestTokenFormatter(unit.TestCase): def test_restore_padding(self): # 'a' will result in '==' padding, 'aa' will result in '=' padding, and # 'aaa' will result in no padding. - strings_to_test = ['a', 'aa', 'aaa'] - - for string in strings_to_test: - encoded_string = base64.urlsafe_b64encode(string) + binary_to_test = [b'a', b'aa', b'aaa'] + + for binary in binary_to_test: + # base64.urlsafe_b64encode takes six.binary_type and returns + # six.binary_type. + encoded_string = base64.urlsafe_b64encode(binary) + encoded_string = encoded_string.decode('utf-8') + # encoded_string is now six.text_type. encoded_str_without_padding = encoded_string.rstrip('=') self.assertFalse(encoded_str_without_padding.endswith('=')) encoded_str_with_padding_restored = ( @@ -231,36 +242,57 @@ class TestTokenFormatter(unit.TestCase): second_value = uuid.uuid4().hex payload = (first_value, second_value) msgpack_payload = msgpack.packb(payload) + # msgpack_payload is six.binary_type. + + tf = token_formatters.TokenFormatter() - # NOTE(lbragstad): This method perserves the way that keystone used to + # NOTE(lbragstad): This method preserves the way that keystone used to # percent encode the tokens, prior to bug #1491926. def legacy_pack(payload): - tf = token_formatters.TokenFormatter() + # payload is six.binary_type. encrypted_payload = tf.crypto.encrypt(payload) + # encrypted_payload is six.binary_type. # the encrypted_payload is returned with padding appended - self.assertTrue(encrypted_payload.endswith('=')) + self.assertTrue(encrypted_payload.endswith(b'=')) # using urllib.parse.quote will percent encode the padding, like # keystone did in Kilo. percent_encoded_payload = urllib.parse.quote(encrypted_payload) + # percent_encoded_payload is six.text_type. - # ensure that the padding was actaully percent encoded + # ensure that the padding was actually percent encoded self.assertTrue(percent_encoded_payload.endswith('%3D')) return percent_encoded_payload token_with_legacy_padding = legacy_pack(msgpack_payload) - tf = token_formatters.TokenFormatter() + # token_with_legacy_padding is six.text_type. # demonstrate the we can validate a payload that has been percent # encoded with the Fernet logic that existed in Kilo serialized_payload = tf.unpack(token_with_legacy_padding) + # serialized_payload is six.binary_type. returned_payload = msgpack.unpackb(serialized_payload) - self.assertEqual(first_value, returned_payload[0]) - self.assertEqual(second_value, returned_payload[1]) + # returned_payload contains six.binary_type. + self.assertEqual(first_value, returned_payload[0].decode('utf-8')) + self.assertEqual(second_value, returned_payload[1].decode('utf-8')) class TestPayloads(unit.TestCase): + def assertTimestampsEqual(self, expected, actual): + # The timestamp that we get back when parsing the payload may not + # exactly match the timestamp that was put in the payload due to + # conversion to and from a float. + + exp_time = timeutils.parse_isotime(expected) + actual_time = timeutils.parse_isotime(actual) + + # the granularity of timestamp string is microseconds and it's only the + # last digit in the representation that's different, so use a delta + # just above nanoseconds. + return self.assertCloseEnoughForGovernmentWork(exp_time, actual_time, + delta=1e-05) + def test_uuid_hex_to_byte_conversions(self): payload_cls = token_formatters.BasePayload @@ -274,249 +306,137 @@ class TestPayloads(unit.TestCase): expected_uuid_in_bytes) self.assertEqual(expected_hex_uuid, actual_hex_uuid) - def test_time_string_to_int_conversions(self): + def test_time_string_to_float_conversions(self): payload_cls = token_formatters.BasePayload - expected_time_str = utils.isotime(subsecond=True) - time_obj = timeutils.parse_isotime(expected_time_str) - expected_time_int = ( + original_time_str = utils.isotime(subsecond=True) + time_obj = timeutils.parse_isotime(original_time_str) + expected_time_float = ( (timeutils.normalize_time(time_obj) - datetime.datetime.utcfromtimestamp(0)).total_seconds()) - actual_time_int = payload_cls._convert_time_string_to_int( - expected_time_str) - self.assertEqual(expected_time_int, actual_time_int) - - actual_time_str = payload_cls._convert_int_to_time_string( - actual_time_int) + # NOTE(lbragstad): The token expiration time for Fernet tokens is + # passed in the payload of the token. This is different from the token + # creation time, which is handled by Fernet and doesn't support + # subsecond precision because it is a timestamp integer. + self.assertIsInstance(expected_time_float, float) + + actual_time_float = payload_cls._convert_time_string_to_float( + original_time_str) + self.assertIsInstance(actual_time_float, float) + self.assertEqual(expected_time_float, actual_time_float) + + # Generate expected_time_str using the same time float. Using + # original_time_str from utils.isotime will occasionally fail due to + # floating point rounding differences. + time_object = datetime.datetime.utcfromtimestamp(actual_time_float) + expected_time_str = utils.isotime(time_object, subsecond=True) + + actual_time_str = payload_cls._convert_float_to_time_string( + actual_time_float) self.assertEqual(expected_time_str, actual_time_str) - def test_unscoped_payload(self): - exp_user_id = uuid.uuid4().hex - exp_methods = ['password'] + def _test_payload(self, payload_class, exp_user_id=None, exp_methods=None, + exp_project_id=None, exp_domain_id=None, + exp_trust_id=None, exp_federated_info=None, + exp_access_token_id=None): + exp_user_id = exp_user_id or uuid.uuid4().hex + exp_methods = exp_methods or ['password'] exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True) exp_audit_ids = [provider.random_urlsafe_str()] - payload = token_formatters.UnscopedPayload.assemble( - exp_user_id, exp_methods, exp_expires_at, exp_audit_ids) + payload = payload_class.assemble( + exp_user_id, exp_methods, exp_project_id, exp_domain_id, + exp_expires_at, exp_audit_ids, exp_trust_id, exp_federated_info, + exp_access_token_id) - (user_id, methods, expires_at, audit_ids) = ( - token_formatters.UnscopedPayload.disassemble(payload)) + (user_id, methods, project_id, + domain_id, expires_at, audit_ids, + trust_id, federated_info, + access_token_id) = payload_class.disassemble(payload) self.assertEqual(exp_user_id, user_id) self.assertEqual(exp_methods, methods) - self.assertEqual(exp_expires_at, expires_at) + self.assertTimestampsEqual(exp_expires_at, expires_at) self.assertEqual(exp_audit_ids, audit_ids) - - def test_project_scoped_payload(self): - exp_user_id = uuid.uuid4().hex - exp_methods = ['password'] - exp_project_id = uuid.uuid4().hex - exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True) - exp_audit_ids = [provider.random_urlsafe_str()] - - payload = token_formatters.ProjectScopedPayload.assemble( - exp_user_id, exp_methods, exp_project_id, exp_expires_at, - exp_audit_ids) - - (user_id, methods, project_id, expires_at, audit_ids) = ( - token_formatters.ProjectScopedPayload.disassemble(payload)) - - self.assertEqual(exp_user_id, user_id) - self.assertEqual(exp_methods, methods) self.assertEqual(exp_project_id, project_id) - self.assertEqual(exp_expires_at, expires_at) - self.assertEqual(exp_audit_ids, audit_ids) + self.assertEqual(exp_domain_id, domain_id) + self.assertEqual(exp_trust_id, trust_id) + self.assertEqual(exp_access_token_id, access_token_id) - def test_domain_scoped_payload(self): - exp_user_id = uuid.uuid4().hex - exp_methods = ['password'] - exp_domain_id = uuid.uuid4().hex - exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True) - exp_audit_ids = [provider.random_urlsafe_str()] + if exp_federated_info: + self.assertDictEqual(exp_federated_info, federated_info) + else: + self.assertIsNone(federated_info) - payload = token_formatters.DomainScopedPayload.assemble( - exp_user_id, exp_methods, exp_domain_id, exp_expires_at, - exp_audit_ids) + def test_unscoped_payload(self): + self._test_payload(token_formatters.UnscopedPayload) - (user_id, methods, domain_id, expires_at, audit_ids) = ( - token_formatters.DomainScopedPayload.disassemble(payload)) + def test_project_scoped_payload(self): + self._test_payload(token_formatters.ProjectScopedPayload, + exp_project_id=uuid.uuid4().hex) - self.assertEqual(exp_user_id, user_id) - self.assertEqual(exp_methods, methods) - self.assertEqual(exp_domain_id, domain_id) - self.assertEqual(exp_expires_at, expires_at) - self.assertEqual(exp_audit_ids, audit_ids) + def test_domain_scoped_payload(self): + self._test_payload(token_formatters.DomainScopedPayload, + exp_domain_id=uuid.uuid4().hex) def test_domain_scoped_payload_with_default_domain(self): - exp_user_id = uuid.uuid4().hex - exp_methods = ['password'] - exp_domain_id = CONF.identity.default_domain_id - exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True) - exp_audit_ids = [provider.random_urlsafe_str()] - - payload = token_formatters.DomainScopedPayload.assemble( - exp_user_id, exp_methods, exp_domain_id, exp_expires_at, - exp_audit_ids) - - (user_id, methods, domain_id, expires_at, audit_ids) = ( - token_formatters.DomainScopedPayload.disassemble(payload)) - - self.assertEqual(exp_user_id, user_id) - self.assertEqual(exp_methods, methods) - self.assertEqual(exp_domain_id, domain_id) - self.assertEqual(exp_expires_at, expires_at) - self.assertEqual(exp_audit_ids, audit_ids) + self._test_payload(token_formatters.DomainScopedPayload, + exp_domain_id=CONF.identity.default_domain_id) def test_trust_scoped_payload(self): - exp_user_id = uuid.uuid4().hex - exp_methods = ['password'] - exp_project_id = uuid.uuid4().hex - exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True) - exp_audit_ids = [provider.random_urlsafe_str()] - exp_trust_id = uuid.uuid4().hex - - payload = token_formatters.TrustScopedPayload.assemble( - exp_user_id, exp_methods, exp_project_id, exp_expires_at, - exp_audit_ids, exp_trust_id) - - (user_id, methods, project_id, expires_at, audit_ids, trust_id) = ( - token_formatters.TrustScopedPayload.disassemble(payload)) - - self.assertEqual(exp_user_id, user_id) - self.assertEqual(exp_methods, methods) - self.assertEqual(exp_project_id, project_id) - self.assertEqual(exp_expires_at, expires_at) - self.assertEqual(exp_audit_ids, audit_ids) - self.assertEqual(exp_trust_id, trust_id) - - def _test_unscoped_payload_with_user_id(self, exp_user_id): - exp_methods = ['password'] - exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True) - exp_audit_ids = [provider.random_urlsafe_str()] - - payload = token_formatters.UnscopedPayload.assemble( - exp_user_id, exp_methods, exp_expires_at, exp_audit_ids) - - (user_id, methods, expires_at, audit_ids) = ( - token_formatters.UnscopedPayload.disassemble(payload)) - - self.assertEqual(exp_user_id, user_id) - self.assertEqual(exp_methods, methods) - self.assertEqual(exp_expires_at, expires_at) - self.assertEqual(exp_audit_ids, audit_ids) + self._test_payload(token_formatters.TrustScopedPayload, + exp_project_id=uuid.uuid4().hex, + exp_trust_id=uuid.uuid4().hex) def test_unscoped_payload_with_non_uuid_user_id(self): - self._test_unscoped_payload_with_user_id('someNonUuidUserId') + self._test_payload(token_formatters.UnscopedPayload, + exp_user_id='someNonUuidUserId') def test_unscoped_payload_with_16_char_non_uuid_user_id(self): - self._test_unscoped_payload_with_user_id('0123456789abcdef') - - def _test_project_scoped_payload_with_ids(self, exp_user_id, - exp_project_id): - exp_methods = ['password'] - exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True) - exp_audit_ids = [provider.random_urlsafe_str()] + self._test_payload(token_formatters.UnscopedPayload, + exp_user_id='0123456789abcdef') - payload = token_formatters.ProjectScopedPayload.assemble( - exp_user_id, exp_methods, exp_project_id, exp_expires_at, - exp_audit_ids) + def test_project_scoped_payload_with_non_uuid_ids(self): + self._test_payload(token_formatters.ProjectScopedPayload, + exp_user_id='someNonUuidUserId', + exp_project_id='someNonUuidProjectId') - (user_id, methods, project_id, expires_at, audit_ids) = ( - token_formatters.ProjectScopedPayload.disassemble(payload)) - - self.assertEqual(exp_user_id, user_id) - self.assertEqual(exp_methods, methods) - self.assertEqual(exp_project_id, project_id) - self.assertEqual(exp_expires_at, expires_at) - self.assertEqual(exp_audit_ids, audit_ids) - - def test_project_scoped_payload_with_non_uuid_user_id(self): - self._test_project_scoped_payload_with_ids('someNonUuidUserId', - 'someNonUuidProjectId') - - def test_project_scoped_payload_with_16_char_non_uuid_user_id(self): - self._test_project_scoped_payload_with_ids('0123456789abcdef', - '0123456789abcdef') - - def _test_domain_scoped_payload_with_user_id(self, exp_user_id): - exp_methods = ['password'] - exp_domain_id = uuid.uuid4().hex - exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True) - exp_audit_ids = [provider.random_urlsafe_str()] - - payload = token_formatters.DomainScopedPayload.assemble( - exp_user_id, exp_methods, exp_domain_id, exp_expires_at, - exp_audit_ids) - - (user_id, methods, domain_id, expires_at, audit_ids) = ( - token_formatters.DomainScopedPayload.disassemble(payload)) - - self.assertEqual(exp_user_id, user_id) - self.assertEqual(exp_methods, methods) - self.assertEqual(exp_domain_id, domain_id) - self.assertEqual(exp_expires_at, expires_at) - self.assertEqual(exp_audit_ids, audit_ids) + def test_project_scoped_payload_with_16_char_non_uuid_ids(self): + self._test_payload(token_formatters.ProjectScopedPayload, + exp_user_id='0123456789abcdef', + exp_project_id='0123456789abcdef') def test_domain_scoped_payload_with_non_uuid_user_id(self): - self._test_domain_scoped_payload_with_user_id('nonUuidUserId') + self._test_payload(token_formatters.DomainScopedPayload, + exp_user_id='nonUuidUserId', + exp_domain_id=uuid.uuid4().hex) def test_domain_scoped_payload_with_16_char_non_uuid_user_id(self): - self._test_domain_scoped_payload_with_user_id('0123456789abcdef') - - def _test_trust_scoped_payload_with_ids(self, exp_user_id, exp_project_id): - exp_methods = ['password'] - exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True) - exp_audit_ids = [provider.random_urlsafe_str()] - exp_trust_id = uuid.uuid4().hex - - payload = token_formatters.TrustScopedPayload.assemble( - exp_user_id, exp_methods, exp_project_id, exp_expires_at, - exp_audit_ids, exp_trust_id) - - (user_id, methods, project_id, expires_at, audit_ids, trust_id) = ( - token_formatters.TrustScopedPayload.disassemble(payload)) - - self.assertEqual(exp_user_id, user_id) - self.assertEqual(exp_methods, methods) - self.assertEqual(exp_project_id, project_id) - self.assertEqual(exp_expires_at, expires_at) - self.assertEqual(exp_audit_ids, audit_ids) - self.assertEqual(exp_trust_id, trust_id) - - def test_trust_scoped_payload_with_non_uuid_user_id(self): - self._test_trust_scoped_payload_with_ids('someNonUuidUserId', - 'someNonUuidProjectId') - - def test_trust_scoped_payload_with_16_char_non_uuid_user_id(self): - self._test_trust_scoped_payload_with_ids('0123456789abcdef', - '0123456789abcdef') + self._test_payload(token_formatters.DomainScopedPayload, + exp_user_id='0123456789abcdef', + exp_domain_id=uuid.uuid4().hex) + + def test_trust_scoped_payload_with_non_uuid_ids(self): + self._test_payload(token_formatters.TrustScopedPayload, + exp_user_id='someNonUuidUserId', + exp_project_id='someNonUuidProjectId', + exp_trust_id=uuid.uuid4().hex) + + def test_trust_scoped_payload_with_16_char_non_uuid_ids(self): + self._test_payload(token_formatters.TrustScopedPayload, + exp_user_id='0123456789abcdef', + exp_project_id='0123456789abcdef', + exp_trust_id=uuid.uuid4().hex) def _test_federated_payload_with_ids(self, exp_user_id, exp_group_id): - exp_methods = ['password'] - exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True) - exp_audit_ids = [provider.random_urlsafe_str()] exp_federated_info = {'group_ids': [{'id': exp_group_id}], 'idp_id': uuid.uuid4().hex, 'protocol_id': uuid.uuid4().hex} - payload = token_formatters.FederatedUnscopedPayload.assemble( - exp_user_id, exp_methods, exp_expires_at, exp_audit_ids, - exp_federated_info) - - (user_id, methods, expires_at, audit_ids, federated_info) = ( - token_formatters.FederatedUnscopedPayload.disassemble(payload)) - - self.assertEqual(exp_user_id, user_id) - self.assertEqual(exp_methods, methods) - self.assertEqual(exp_expires_at, expires_at) - self.assertEqual(exp_audit_ids, audit_ids) - self.assertEqual(exp_federated_info['group_ids'][0]['id'], - federated_info['group_ids'][0]['id']) - self.assertEqual(exp_federated_info['idp_id'], - federated_info['idp_id']) - self.assertEqual(exp_federated_info['protocol_id'], - federated_info['protocol_id']) + self._test_payload(token_formatters.FederatedUnscopedPayload, + exp_user_id=exp_user_id, + exp_federated_info=exp_federated_info) def test_federated_payload_with_non_uuid_ids(self): self._test_federated_payload_with_ids('someNonUuidUserId', @@ -527,56 +447,31 @@ class TestPayloads(unit.TestCase): '0123456789abcdef') def test_federated_project_scoped_payload(self): - exp_user_id = 'someNonUuidUserId' - exp_methods = ['token'] - exp_project_id = uuid.uuid4().hex - exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True) - exp_audit_ids = [provider.random_urlsafe_str()] exp_federated_info = {'group_ids': [{'id': 'someNonUuidGroupId'}], 'idp_id': uuid.uuid4().hex, 'protocol_id': uuid.uuid4().hex} - payload = token_formatters.FederatedProjectScopedPayload.assemble( - exp_user_id, exp_methods, exp_project_id, exp_expires_at, - exp_audit_ids, exp_federated_info) - - (user_id, methods, project_id, expires_at, audit_ids, - federated_info) = ( - token_formatters.FederatedProjectScopedPayload.disassemble( - payload)) - - self.assertEqual(exp_user_id, user_id) - self.assertEqual(exp_methods, methods) - self.assertEqual(exp_project_id, project_id) - self.assertEqual(exp_expires_at, expires_at) - self.assertEqual(exp_audit_ids, audit_ids) - self.assertDictEqual(exp_federated_info, federated_info) + self._test_payload(token_formatters.FederatedProjectScopedPayload, + exp_user_id='someNonUuidUserId', + exp_methods=['token'], + exp_project_id=uuid.uuid4().hex, + exp_federated_info=exp_federated_info) def test_federated_domain_scoped_payload(self): - exp_user_id = 'someNonUuidUserId' - exp_methods = ['token'] - exp_domain_id = uuid.uuid4().hex - exp_expires_at = utils.isotime(timeutils.utcnow(), subsecond=True) - exp_audit_ids = [provider.random_urlsafe_str()] exp_federated_info = {'group_ids': [{'id': 'someNonUuidGroupId'}], 'idp_id': uuid.uuid4().hex, 'protocol_id': uuid.uuid4().hex} - payload = token_formatters.FederatedDomainScopedPayload.assemble( - exp_user_id, exp_methods, exp_domain_id, exp_expires_at, - exp_audit_ids, exp_federated_info) + self._test_payload(token_formatters.FederatedDomainScopedPayload, + exp_user_id='someNonUuidUserId', + exp_methods=['token'], + exp_domain_id=uuid.uuid4().hex, + exp_federated_info=exp_federated_info) - (user_id, methods, domain_id, expires_at, audit_ids, - federated_info) = ( - token_formatters.FederatedDomainScopedPayload.disassemble( - payload)) - - self.assertEqual(exp_user_id, user_id) - self.assertEqual(exp_methods, methods) - self.assertEqual(exp_domain_id, domain_id) - self.assertEqual(exp_expires_at, expires_at) - self.assertEqual(exp_audit_ids, audit_ids) - self.assertDictEqual(exp_federated_info, federated_info) + def test_oauth_scoped_payload(self): + self._test_payload(token_formatters.OauthScopedPayload, + exp_project_id=uuid.uuid4().hex, + exp_access_token_id=uuid.uuid4().hex) class TestFernetKeyRotation(unit.TestCase): @@ -610,7 +505,7 @@ class TestFernetKeyRotation(unit.TestCase): static set of keys, and simply shuffling them, would fail such a test). """ - # Load the keys into a list. + # Load the keys into a list, keys is list of six.text_type. keys = fernet_utils.load_keys() # Sort the list of keys by the keys themselves (they were previously @@ -620,7 +515,8 @@ class TestFernetKeyRotation(unit.TestCase): # Create the thumbprint using all keys in the repository. signature = hashlib.sha1() for key in keys: - signature.update(key) + # Need to convert key to six.binary_type for update. + signature.update(key.encode('utf-8')) return signature.hexdigest() def assertRepositoryState(self, expected_size): diff --git a/keystone-moon/keystone/tests/unit/token/test_provider.py b/keystone-moon/keystone/tests/unit/token/test_provider.py index be831484..7093f3ba 100644 --- a/keystone-moon/keystone/tests/unit/token/test_provider.py +++ b/keystone-moon/keystone/tests/unit/token/test_provider.py @@ -24,7 +24,7 @@ class TestRandomStrings(unit.BaseTestCase): def test_strings_can_be_converted_to_bytes(self): s = provider.random_urlsafe_str() - self.assertTrue(isinstance(s, six.string_types)) + self.assertIsInstance(s, six.text_type) b = provider.random_urlsafe_str_to_bytes(s) - self.assertTrue(isinstance(b, bytes)) + self.assertIsInstance(b, six.binary_type) diff --git a/keystone-moon/keystone/tests/unit/token/test_token_data_helper.py b/keystone-moon/keystone/tests/unit/token/test_token_data_helper.py index 6114b723..9e8c3889 100644 --- a/keystone-moon/keystone/tests/unit/token/test_token_data_helper.py +++ b/keystone-moon/keystone/tests/unit/token/test_token_data_helper.py @@ -28,7 +28,8 @@ class TestTokenDataHelper(unit.TestCase): def test_v3_token_data_helper_populate_audit_info_string(self): token_data = {} - audit_info = base64.urlsafe_b64encode(uuid.uuid4().bytes)[:-2] + audit_info_bytes = base64.urlsafe_b64encode(uuid.uuid4().bytes)[:-2] + audit_info = audit_info_bytes.decode('utf-8') self.v3_data_helper._populate_audit_info(token_data, audit_info) self.assertIn(audit_info, token_data['audit_ids']) self.assertThat(token_data['audit_ids'], matchers.HasLength(2)) diff --git a/keystone-moon/keystone/tests/unit/token/test_token_model.py b/keystone-moon/keystone/tests/unit/token/test_token_model.py index f1398491..1cb0ef55 100644 --- a/keystone-moon/keystone/tests/unit/token/test_token_model.py +++ b/keystone-moon/keystone/tests/unit/token/test_token_model.py @@ -17,8 +17,8 @@ from oslo_config import cfg from oslo_utils import timeutils from six.moves import range -from keystone.contrib.federation import constants as federation_constants from keystone import exception +from keystone.federation import constants as federation_constants from keystone.models import token_model from keystone.tests.unit import core from keystone.tests.unit import test_token_provider diff --git a/keystone-moon/keystone/tests/unit/trust/__init__.py b/keystone-moon/keystone/tests/unit/trust/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/keystone-moon/keystone/tests/unit/trust/test_backends.py b/keystone-moon/keystone/tests/unit/trust/test_backends.py new file mode 100644 index 00000000..05df866f --- /dev/null +++ b/keystone-moon/keystone/tests/unit/trust/test_backends.py @@ -0,0 +1,172 @@ +# 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 datetime +import uuid + +from oslo_utils import timeutils +from six.moves import range + +from keystone import exception + + +class TrustTests(object): + def create_sample_trust(self, new_id, remaining_uses=None): + self.trustor = self.user_foo + self.trustee = self.user_two + expires_at = datetime.datetime.utcnow().replace(year=2032) + trust_data = (self.trust_api.create_trust + (new_id, + {'trustor_user_id': self.trustor['id'], + 'trustee_user_id': self.user_two['id'], + 'project_id': self.tenant_bar['id'], + 'expires_at': expires_at, + 'impersonation': True, + 'remaining_uses': remaining_uses}, + roles=[{"id": "member"}, + {"id": "other"}, + {"id": "browser"}])) + return trust_data + + def test_delete_trust(self): + new_id = uuid.uuid4().hex + trust_data = self.create_sample_trust(new_id) + trust_id = trust_data['id'] + self.assertIsNotNone(trust_data) + trust_data = self.trust_api.get_trust(trust_id) + self.assertEqual(new_id, trust_data['id']) + self.trust_api.delete_trust(trust_id) + self.assertRaises(exception.TrustNotFound, + self.trust_api.get_trust, + trust_id) + + def test_delete_trust_not_found(self): + trust_id = uuid.uuid4().hex + self.assertRaises(exception.TrustNotFound, + self.trust_api.delete_trust, + trust_id) + + def test_get_trust(self): + new_id = uuid.uuid4().hex + trust_data = self.create_sample_trust(new_id) + trust_id = trust_data['id'] + self.assertIsNotNone(trust_data) + trust_data = self.trust_api.get_trust(trust_id) + self.assertEqual(new_id, trust_data['id']) + self.trust_api.delete_trust(trust_data['id']) + + def test_get_deleted_trust(self): + new_id = uuid.uuid4().hex + trust_data = self.create_sample_trust(new_id) + self.assertIsNotNone(trust_data) + self.assertIsNone(trust_data['deleted_at']) + self.trust_api.delete_trust(new_id) + self.assertRaises(exception.TrustNotFound, + self.trust_api.get_trust, + new_id) + deleted_trust = self.trust_api.get_trust(trust_data['id'], + deleted=True) + self.assertEqual(trust_data['id'], deleted_trust['id']) + self.assertIsNotNone(deleted_trust.get('deleted_at')) + + def test_create_trust(self): + new_id = uuid.uuid4().hex + trust_data = self.create_sample_trust(new_id) + + self.assertEqual(new_id, trust_data['id']) + self.assertEqual(self.trustee['id'], trust_data['trustee_user_id']) + self.assertEqual(self.trustor['id'], trust_data['trustor_user_id']) + self.assertTrue(timeutils.normalize_time(trust_data['expires_at']) > + timeutils.utcnow()) + + self.assertEqual([{'id': 'member'}, + {'id': 'other'}, + {'id': 'browser'}], trust_data['roles']) + + def test_list_trust_by_trustee(self): + for i in range(3): + self.create_sample_trust(uuid.uuid4().hex) + trusts = self.trust_api.list_trusts_for_trustee(self.trustee['id']) + self.assertEqual(3, len(trusts)) + self.assertEqual(trusts[0]["trustee_user_id"], self.trustee['id']) + trusts = self.trust_api.list_trusts_for_trustee(self.trustor['id']) + self.assertEqual(0, len(trusts)) + + def test_list_trust_by_trustor(self): + for i in range(3): + self.create_sample_trust(uuid.uuid4().hex) + trusts = self.trust_api.list_trusts_for_trustor(self.trustor['id']) + self.assertEqual(3, len(trusts)) + self.assertEqual(trusts[0]["trustor_user_id"], self.trustor['id']) + trusts = self.trust_api.list_trusts_for_trustor(self.trustee['id']) + self.assertEqual(0, len(trusts)) + + def test_list_trusts(self): + for i in range(3): + self.create_sample_trust(uuid.uuid4().hex) + trusts = self.trust_api.list_trusts() + self.assertEqual(3, len(trusts)) + + def test_trust_has_remaining_uses_positive(self): + # create a trust with limited uses, check that we have uses left + trust_data = self.create_sample_trust(uuid.uuid4().hex, + remaining_uses=5) + self.assertEqual(5, trust_data['remaining_uses']) + # create a trust with unlimited uses, check that we have uses left + trust_data = self.create_sample_trust(uuid.uuid4().hex) + self.assertIsNone(trust_data['remaining_uses']) + + def test_trust_has_remaining_uses_negative(self): + # try to create a trust with no remaining uses, check that it fails + self.assertRaises(exception.ValidationError, + self.create_sample_trust, + uuid.uuid4().hex, + remaining_uses=0) + # try to create a trust with negative remaining uses, + # check that it fails + self.assertRaises(exception.ValidationError, + self.create_sample_trust, + uuid.uuid4().hex, + remaining_uses=-12) + + def test_consume_use(self): + # consume a trust repeatedly until it has no uses anymore + trust_data = self.create_sample_trust(uuid.uuid4().hex, + remaining_uses=2) + self.trust_api.consume_use(trust_data['id']) + t = self.trust_api.get_trust(trust_data['id']) + self.assertEqual(1, t['remaining_uses']) + self.trust_api.consume_use(trust_data['id']) + # This was the last use, the trust isn't available anymore + self.assertRaises(exception.TrustNotFound, + self.trust_api.get_trust, + trust_data['id']) + + def test_duplicate_trusts_not_allowed(self): + self.trustor = self.user_foo + self.trustee = self.user_two + trust_data = {'trustor_user_id': self.trustor['id'], + 'trustee_user_id': self.user_two['id'], + 'project_id': self.tenant_bar['id'], + 'expires_at': timeutils.parse_isotime( + '2032-02-18T18:10:00Z'), + 'impersonation': True, + 'remaining_uses': None} + roles = [{"id": "member"}, + {"id": "other"}, + {"id": "browser"}] + self.trust_api.create_trust(uuid.uuid4().hex, trust_data, roles) + self.assertRaises(exception.Conflict, + self.trust_api.create_trust, + uuid.uuid4().hex, + trust_data, + roles) diff --git a/keystone-moon/keystone/tests/unit/utils.py b/keystone-moon/keystone/tests/unit/utils.py index 17d1de81..e3e49e70 100644 --- a/keystone-moon/keystone/tests/unit/utils.py +++ b/keystone-moon/keystone/tests/unit/utils.py @@ -17,13 +17,10 @@ import os import time import uuid -from oslo_log import log import six from testtools import testcase -LOG = log.getLogger(__name__) - TZ = None @@ -72,7 +69,6 @@ def wip(message): >>> pass """ - def _wip(f): @six.wraps(f) def run_test(*args, **kwargs): -- cgit 1.2.3-korg