Support domain in keystone lookups

Implements: 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.

Release note added.

Story: 2005523
Task: 30642

Change-Id: I2b02787bd8883ced631b81174cee9134445bf170
This commit is contained in:
Sampat P 2019-06-05 15:07:09 -04:00
parent c4076e12c8
commit 169a35e059
3 changed files with 340 additions and 11 deletions

View File

@ -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

View File

@ -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):

View File

@ -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.