diff --git a/heat/engine/clients/os/keystone/__init__.py b/heat/engine/clients/os/keystone/__init__.py index 1847053387..1fa5b65277 100644 --- a/heat/engine/clients/os/keystone/__init__.py +++ b/heat/engine/clients/os/keystone/__init__.py @@ -11,6 +11,8 @@ # License for the specific language governing permissions and limitations # under the License. +import re + from keystoneauth1 import exceptions as ks_exceptions from heat.common import exception @@ -37,26 +39,59 @@ class KeystoneClientPlugin(client_plugin.ClientPlugin): def is_conflict(self, ex): return isinstance(ex, ks_exceptions.Conflict) - def get_role_id(self, role): + def parse_entity_with_domain(self, entity_with_domain, entity_type): + """Parse keystone entity user/role/project with domain. + + entity_with_domain should be in entity{domain} format. + + Returns a tuple of (entity, domain). + """ + try: + match = re.search(r"\{(.*?)\}$", entity_with_domain) + if match: + entity = entity_with_domain[:match.start()] + domain = match.group(1) + domain = self.get_domain_id(domain) + return (entity, domain) + else: + return (entity_with_domain, None) + except Exception: + raise exception.EntityNotFound(entity=entity_type, + name=entity_with_domain) + + def get_role_id(self, role, domain=None): + if role is None: + return None + + if not domain: + role, domain = self.parse_entity_with_domain(role, 'KeystoneRole') + try: role_obj = self.client().client.roles.get(role) return role_obj.id except ks_exceptions.NotFound: - role_list = self.client().client.roles.list(name=role) + role_list = self.client().client.roles.list(name=role, + domain=domain) for role_obj in role_list: if role_obj.name == role: return role_obj.id raise exception.EntityNotFound(entity='KeystoneRole', name=role) - def get_project_id(self, project): + def get_project_id(self, project, domain=None): if project is None: return None + + if not domain: + project, domain = self.parse_entity_with_domain(project, + 'KeystoneProject') + try: project_obj = self.client().client.projects.get(project) return project_obj.id except ks_exceptions.NotFound: - project_list = self.client().client.projects.list(name=project) + project_list = self.client().client.projects.list(name=project, + domain=domain) for project_obj in project_list: if project_obj.name == project: return project_obj.id @@ -78,14 +113,20 @@ class KeystoneClientPlugin(client_plugin.ClientPlugin): raise exception.EntityNotFound(entity='KeystoneDomain', name=domain) - def get_group_id(self, group): + def get_group_id(self, group, domain=None): if group is None: return None + + if not domain: + group, domain = self.parse_entity_with_domain(group, + 'KeystoneGroup') + try: group_obj = self.client().client.groups.get(group) return group_obj.id except ks_exceptions.NotFound: - group_list = self.client().client.groups.list(name=group) + group_list = self.client().client.groups.list(name=group, + domain=domain) for group_obj in group_list: if group_obj.name == group: return group_obj.id @@ -109,14 +150,20 @@ class KeystoneClientPlugin(client_plugin.ClientPlugin): raise exception.EntityNotFound(entity='KeystoneService', name=service) - def get_user_id(self, user): + def get_user_id(self, user, domain=None): if user is None: return None + + if not domain: + user, domain = self.parse_entity_with_domain(user, + 'KeystoneUser') + try: user_obj = self.client().client.users.get(user) return user_obj.id except ks_exceptions.NotFound: - user_list = self.client().client.users.list(name=user) + user_list = self.client().client.users.list(name=user, + domain=domain) for user_obj in user_list: if user_obj.name == user: return user_obj.id diff --git a/heat/tests/clients/test_keystone_client.py b/heat/tests/clients/test_keystone_client.py index 21b96205cb..b2ae723ca2 100644 --- a/heat/tests/clients/test_keystone_client.py +++ b/heat/tests/clients/test_keystone_client.py @@ -21,6 +21,39 @@ from heat.engine.clients.os.keystone import keystone_constraints as ks_constr from heat.tests import common +class KeystoneClientParseEntityTest(common.HeatTestCase): + + sample_uuid = '477e8273-60a7-4c41-b683-fdb0bc7cd151' + + @mock.patch.object(keystone.KeystoneClientPlugin, 'client') + def test_parse_entity_with_domain(self, client_keystone): + client_keystone.return_value = self._client + client_plugin = keystone.KeystoneClientPlugin( + context=mock.MagicMock() + ) + client_plugin.get_domain_id = mock.MagicMock() + client_plugin.get_domain_id.return_value = self.sample_uuid + self.assertEqual(client_plugin.parse_entity_with_domain( + 'entity{domain}', 'entity_type'), ('entity', self.sample_uuid) + ) + + @mock.patch.object(keystone.KeystoneClientPlugin, 'client') + def test_parse_entity_without_domain(self, client_keystone): + client_keystone.return_value = self._client + client_plugin = keystone.KeystoneClientPlugin( + context=mock.MagicMock() + ) + client_plugin.get_domain_id = mock.MagicMock() + client_plugin.get_domain_id.return_value = self.sample_uuid + self.assertEqual(client_plugin.parse_entity_with_domain( + 'entity', 'entity_type'), ('entity', None) + ) + + def setUp(self): + super(KeystoneClientParseEntityTest, self).setUp() + self._client = mock.MagicMock() + + class KeystoneRoleConstraintTest(common.HeatTestCase): def test_expected_exceptions(self): @@ -302,11 +335,16 @@ class KeystoneClientPluginRoleTest(common.HeatTestCase): sample_uuid = '477e8273-60a7-4c41-b683-fdb0bc7cd152' sample_name = 'sample_role' + sample_name_and_domain = 'sample_role{sample_domain}' + sample_domain_uuid = '577e8273-60a7-4c41-b683-fdb0bc7cd152' + sample_domain_name = 'sample_domain' + sample_name_and_domain_invalid_input = 'sample_role@@' def _get_mock_role(self): role = mock.MagicMock() role.id = self.sample_uuid role.name = self.sample_name + role.name_and_domain = self.sample_name_and_domain return role def setUp(self): @@ -347,6 +385,29 @@ class KeystoneClientPluginRoleTest(common.HeatTestCase): self._client.client.roles.get, self.sample_name) self._client.client.roles.list.assert_called_once_with( + domain=None, name=self.sample_name) + + @mock.patch.object(keystone.KeystoneClientPlugin, 'client') + def test_get_role_id_with_name_and_domain(self, client_keystone): + self._client.client.roles.get.side_effect = (keystone_exceptions + .NotFound) + self._client.client.roles.list.return_value = [ + self._get_mock_role() + ] + + client_keystone.return_value = self._client + client_plugin = keystone.KeystoneClientPlugin( + context=mock.MagicMock() + ) + + self.assertEqual(self.sample_uuid, client_plugin.get_role_id( + self.sample_name_and_domain)) + + self.assertRaises(keystone_exceptions.NotFound, + self._client.client.roles.get, + self.sample_name) + self._client.client.roles.list.assert_called_once_with( + domain=client_plugin.get_domain_id(self.sample_domain_uuid), name=self.sample_name) @mock.patch.object(keystone.KeystoneClientPlugin, 'client') @@ -371,18 +432,62 @@ class KeystoneClientPluginRoleTest(common.HeatTestCase): self._client.client.roles.get, self.sample_name) self._client.client.roles.list.assert_called_once_with( + domain=None, name=self.sample_name) + + @mock.patch.object(keystone.KeystoneClientPlugin, 'client') + def test_get_role_id_with_domain_not_found(self, client_keystone): + self._client.client.roles.get.side_effect = (keystone_exceptions + .NotFound) + self._client.client.roles.list.return_value = [ + ] + + client_keystone.return_value = self._client + client_plugin = keystone.KeystoneClientPlugin( + context=mock.MagicMock() + ) + + ex = self.assertRaises(exception.EntityNotFound, + client_plugin.get_role_id, + self.sample_name_and_domain) + msg = ("The KeystoneRole (%(name)s) could not be found." % + {'name': self.sample_name}) + self.assertEqual(msg, six.text_type(ex)) + self.assertRaises(keystone_exceptions.NotFound, + self._client.client.roles.get, + self.sample_name) + self._client.client.roles.list.assert_called_once_with( + domain=client_plugin.get_domain_id(self.sample_domain_uuid), name=self.sample_name) + @mock.patch.object(keystone.KeystoneClientPlugin, 'client') + def test_get_role_id_with_name_and_domain_invalid_input(self, + client_keystone): + self._client.client.roles.get.side_effect = (keystone_exceptions + .NotFound) + self._client.client.roles.list.return_value = [] + + client_keystone.return_value = self._client + client_plugin = keystone.KeystoneClientPlugin( + context=mock.MagicMock() + ) + self.assertRaises(exception.EntityNotFound, + client_plugin.get_role_id, + self.sample_name_and_domain_invalid_input) + class KeystoneClientPluginProjectTest(common.HeatTestCase): - sample_uuid = '477e8273-60a7-4c41-b683-fdb0bc7cd152' sample_name = 'sample_project' + sample_name_and_domain = 'sample_project{sample_domain}' + sample_domain_uuid = '577e8273-60a7-4c41-b683-fdb0bc7cd152' + sample_domain_name = 'sample_domain' + sample_name_and_domain_invalid_input = 'sample_project@@' def _get_mock_project(self): project = mock.MagicMock() project.id = self.sample_uuid project.name = self.sample_name + project.name_and_domain = self.sample_name_and_domain return project def setUp(self): @@ -423,6 +528,29 @@ class KeystoneClientPluginProjectTest(common.HeatTestCase): self._client.client.projects.get, self.sample_name) self._client.client.projects.list.assert_called_once_with( + domain=None, name=self.sample_name) + + @mock.patch.object(keystone.KeystoneClientPlugin, 'client') + def test_get_project_id_with_name_and_domain(self, client_keystone): + self._client.client.projects.get.side_effect = (keystone_exceptions + .NotFound) + self._client.client.projects.list.return_value = [ + self._get_mock_project() + ] + + client_keystone.return_value = self._client + client_plugin = keystone.KeystoneClientPlugin( + context=mock.MagicMock() + ) + + self.assertEqual(self.sample_uuid, client_plugin.get_project_id( + self.sample_name_and_domain)) + + self.assertRaises(keystone_exceptions.NotFound, + self._client.client.projects.get, + self.sample_name) + self._client.client.projects.list.assert_called_once_with( + domain=client_plugin.get_domain_id(self.sample_domain_uuid), name=self.sample_name) @mock.patch.object(keystone.KeystoneClientPlugin, 'client') @@ -447,8 +575,47 @@ class KeystoneClientPluginProjectTest(common.HeatTestCase): self._client.client.projects.get, self.sample_name) self._client.client.projects.list.assert_called_once_with( + domain=None, name=self.sample_name) + + @mock.patch.object(keystone.KeystoneClientPlugin, 'client') + def test_get_project_id_with_domain_not_found(self, client_keystone): + self._client.client.projects.get.side_effect = (keystone_exceptions + .NotFound) + self._client.client.projects.list.return_value = [] + + client_keystone.return_value = self._client + client_plugin = keystone.KeystoneClientPlugin( + context=mock.MagicMock() + ) + + ex = self.assertRaises(exception.EntityNotFound, + client_plugin.get_project_id, + self.sample_name_and_domain) + msg = ("The KeystoneProject (%(name)s) could not be found." % + {'name': self.sample_name}) + self.assertEqual(msg, six.text_type(ex)) + self.assertRaises(keystone_exceptions.NotFound, + self._client.client.projects.get, + self.sample_name) + self._client.client.projects.list.assert_called_once_with( + domain=client_plugin.get_domain_id(self.sample_domain_uuid), name=self.sample_name) + @mock.patch.object(keystone.KeystoneClientPlugin, 'client') + def test_get_project_id_with_name_and_domain_invalid_input( + self, client_keystone): + self._client.client.projects.get.side_effect = (keystone_exceptions + .NotFound) + self._client.client.projects.list.return_value = [] + + client_keystone.return_value = self._client + client_plugin = keystone.KeystoneClientPlugin( + context=mock.MagicMock() + ) + self.assertRaises(exception.EntityNotFound, + client_plugin.get_project_id, + self.sample_name_and_domain_invalid_input) + class KeystoneClientPluginDomainTest(common.HeatTestCase): @@ -530,11 +697,16 @@ class KeystoneClientPluginGroupTest(common.HeatTestCase): sample_uuid = '477e8273-60a7-4c41-b683-fdb0bc7cd152' sample_name = 'sample_group' + sample_name_and_domain = 'sample_group{sample_domain}' + sample_domain_uuid = '577e8273-60a7-4c41-b683-fdb0bc7cd152' + sample_domain_name = 'sample_domain' + sample_name_and_domain_invalid_input = 'sample_group@@' def _get_mock_group(self): group = mock.MagicMock() group.id = self.sample_uuid group.name = self.sample_name + group.name_and_domain = self.sample_name_and_domain return group def setUp(self): @@ -575,6 +747,29 @@ class KeystoneClientPluginGroupTest(common.HeatTestCase): self._client.client.groups.get, self.sample_name) self._client.client.groups.list.assert_called_once_with( + domain=None, name=self.sample_name) + + @mock.patch.object(keystone.KeystoneClientPlugin, 'client') + def test_get_group_id_with_name_and_domain(self, client_keystone): + self._client.client.groups.get.side_effect = (keystone_exceptions + .NotFound) + self._client.client.groups.list.return_value = [ + self._get_mock_group() + ] + + client_keystone.return_value = self._client + client_plugin = keystone.KeystoneClientPlugin( + context=mock.MagicMock() + ) + + self.assertEqual(self.sample_uuid, client_plugin.get_group_id( + self.sample_name_and_domain)) + + self.assertRaises(keystone_exceptions.NotFound, + self._client.client.groups.get, + self.sample_name) + self._client.client.groups.list.assert_called_once_with( + domain=client_plugin.get_domain_id(self.sample_domain_uuid), name=self.sample_name) @mock.patch.object(keystone.KeystoneClientPlugin, 'client') @@ -599,18 +794,63 @@ class KeystoneClientPluginGroupTest(common.HeatTestCase): self._client.client.groups.get, self.sample_name) self._client.client.groups.list.assert_called_once_with( + domain=None, name=self.sample_name) + + @mock.patch.object(keystone.KeystoneClientPlugin, 'client') + def test_get_group_id_with_domain_not_found(self, client_keystone): + self._client.client.groups.get.side_effect = (keystone_exceptions + .NotFound) + self._client.client.groups.list.return_value = [ + ] + + client_keystone.return_value = self._client + client_plugin = keystone.KeystoneClientPlugin( + context=mock.MagicMock() + ) + + ex = self.assertRaises(exception.EntityNotFound, + client_plugin.get_group_id, + self.sample_name_and_domain) + msg = ("The KeystoneGroup (%(name)s) could not be found." % + {'name': self.sample_name}) + self.assertEqual(msg, six.text_type(ex)) + self.assertRaises(keystone_exceptions.NotFound, + self._client.client.groups.get, + self.sample_name) + self._client.client.groups.list.assert_called_once_with( + domain=client_plugin.get_domain_id(self.sample_domain_uuid), name=self.sample_name) + @mock.patch.object(keystone.KeystoneClientPlugin, 'client') + def test_get_group_id_with_name_and_domain_invalid_input( + self, client_keystone): + self._client.client.groups.get.side_effect = (keystone_exceptions + .NotFound) + self._client.client.groups.list.return_value = [] + + client_keystone.return_value = self._client + client_plugin = keystone.KeystoneClientPlugin( + context=mock.MagicMock() + ) + self.assertRaises(exception.EntityNotFound, + client_plugin.get_group_id, + self.sample_name_and_domain_invalid_input) + class KeystoneClientPluginUserTest(common.HeatTestCase): sample_uuid = '477e8273-60a7-4c41-b683-fdb0bc7cd152' sample_name = 'sample_user' + sample_name_and_domain = 'sample_user{sample_domain}' + sample_domain_uuid = '577e8273-60a7-4c41-b683-fdb0bc7cd152' + sample_domain_name = 'sample_domain' + sample_name_and_domain_invalid_input = 'sample_user@@' def _get_mock_user(self): user = mock.MagicMock() user.id = self.sample_uuid user.name = self.sample_name + user.name_and_domain = self.sample_name_and_domain return user def setUp(self): @@ -620,7 +860,6 @@ class KeystoneClientPluginUserTest(common.HeatTestCase): @mock.patch.object(keystone.KeystoneClientPlugin, 'client') def test_get_user_id(self, client_keystone): self._client.client.users.get.return_value = self._get_mock_user() - client_keystone.return_value = self._client client_plugin = keystone.KeystoneClientPlugin( context=mock.MagicMock() @@ -650,6 +889,27 @@ class KeystoneClientPluginUserTest(common.HeatTestCase): self._client.client.users.get, self.sample_name) self._client.client.users.list.assert_called_once_with( + domain=None, name=self.sample_name) + + @mock.patch.object(keystone.KeystoneClientPlugin, 'client') + def test_get_user_id_with_name_and_domain(self, client_keystone): + self._client.client.users.get.side_effect = (keystone_exceptions + .NotFound) + self._client.client.users.list.return_value = [ + self._get_mock_user() + ] + + client_keystone.return_value = self._client + client_plugin = keystone.KeystoneClientPlugin( + context=mock.MagicMock()) + self.assertEqual(self.sample_uuid, client_plugin.get_user_id( + self.sample_name_and_domain)) + + self.assertRaises(keystone_exceptions.NotFound, + self._client.client.users.get, + self.sample_name) + self._client.client.users.list.assert_called_once_with( + domain=client_plugin.get_domain_id(self.sample_domain_uuid), name=self.sample_name) @mock.patch.object(keystone.KeystoneClientPlugin, 'client') @@ -673,7 +933,22 @@ class KeystoneClientPluginUserTest(common.HeatTestCase): self._client.client.users.get, self.sample_name) self._client.client.users.list.assert_called_once_with( - name=self.sample_name) + domain=None, name=self.sample_name) + + @mock.patch.object(keystone.KeystoneClientPlugin, 'client') + def test_get_user_id_with_name_and_domain_invalid_input(self, + client_keystone): + self._client.client.users.get.side_effect = (keystone_exceptions + .NotFound) + self._client.client.users.list.return_value = [] + + client_keystone.return_value = self._client + client_plugin = keystone.KeystoneClientPlugin( + context=mock.MagicMock() + ) + self.assertRaises(exception.EntityNotFound, + client_plugin.get_user_id, + self.sample_name_and_domain_invalid_input) class KeystoneClientPluginRegionTest(common.HeatTestCase): diff --git a/releasenotes/notes/support-domain-in-keystone-lookups-f657da8322f17938.yaml b/releasenotes/notes/support-domain-in-keystone-lookups-f657da8322f17938.yaml new file mode 100644 index 0000000000..a464386ea4 --- /dev/null +++ b/releasenotes/notes/support-domain-in-keystone-lookups-f657da8322f17938.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Supports user, group, role and project lookup across domains. Added domain + parameter to keystone lookup functions. Heat templates now support + user{domain}, group{domain}, role{domain} and project{domain} to support + cross domain lookup. Keystone constrains will also work across domain.