diff --git a/shade/_tasks.py b/shade/_tasks.py index 121605747..dffda0042 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -677,6 +677,11 @@ class RoleDelete(task_manager.Task): return client.keystone_client.roles.delete(**self.args) +class RoleAssignmentList(task_manager.Task): + def main(self, client): + return client.keystone_client.role_assignments.list(**self.args) + + class StackList(task_manager.Task): def main(self, client): return client.heat_client.stacks.list() diff --git a/shade/_utils.py b/shade/_utils.py index ef436630b..f8fa5a58c 100644 --- a/shade/_utils.py +++ b/shade/_utils.py @@ -363,6 +363,56 @@ def normalize_groups(domains): return meta.obj_list_to_dict(ret) +def normalize_role_assignments(assignments): + """Put role_assignments into a form that works with search/get interface. + + Role assignments have the structure:: + + [ + { + "role": { + "id": "--role-id--" + }, + "scope": { + "domain": { + "id": "--domain-id--" + } + }, + "user": { + "id": "--user-id--" + } + }, + ] + + Which is hard to work with in the rest of our interface. Map this to be:: + + [ + { + "id": "--role-id--", + "domain": "--domain-id--", + "user": "--user-id--", + } + ] + + Scope can be "domain" or "project" and "user" can also be "group". + + :param list assignments: A list of dictionaries of role assignments. + + :returns: A list of flattened/normalized role assignment dicts. + """ + new_assignments = [] + for assignment in assignments: + new_val = {'id': assignment['role']['id']} + for scope in ('project', 'domain'): + if scope in assignment['scope']: + new_val[scope] = assignment['scope'][scope]['id'] + for assignee in ('user', 'group'): + if assignee in assignment: + new_val[assignee] = assignment[assignee]['id'] + new_assignments.append(new_val) + return new_assignments + + def valid_kwargs(*valid_args): # This decorator checks if argument passed as **kwargs to a function are # present in valid_args. diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py index 5c9c7d483..c50202424 100644 --- a/shade/operatorcloud.py +++ b/shade/operatorcloud.py @@ -1286,6 +1286,43 @@ class OperatorCloud(openstackcloud.OpenStackCloud): """ return _utils._get_entity(self.search_roles, name_or_id, filters) + def list_role_assignments(self, filters=None): + """List Keystone role assignments + + :param dict filters: Dict of filter conditions. Acceptable keys are:: + + - 'user' (string) - User ID to be used as query filter. + - 'group' (string) - Group ID to be used as query filter. + - 'project' (string) - Project ID to be used as query filter. + - 'domain' (string) - Domain ID to be used as query filter. + - 'role' (string) - Role ID to be used as query filter. + - 'os_inherit_extension_inherited_to' (string) - Return inherited + role assignments for either 'projects' or 'domains' + - 'effective' (boolean) - Return effective role assignments. + - 'include_subtree' (boolean) - Include subtree + + 'user' and 'group' are mutually exclusive, as are 'domain' and + 'project'. + + :returns: a list of dicts containing the role assignment description. + Contains the following attributes:: + + - id: + - user|group: + - project|domain: + + :raises: ``OpenStackCloudException``: if something goes wrong during + the openstack API call. + """ + if not filters: + filters = {} + + with _utils.shade_exceptions("Failed to list role assignments"): + assignments = self.manager.submitTask( + _tasks.RoleAssignmentList(**filters) + ) + return _utils.normalize_role_assignments(assignments) + def create_flavor(self, name, ram, vcpus, disk, flavorid="auto", ephemeral=0, swap=0, rxtx_factor=1.0, is_public=True): """Create a new flavor. diff --git a/shade/tests/functional/test_identity.py b/shade/tests/functional/test_identity.py index b3b31b272..d50be33af 100644 --- a/shade/tests/functional/test_identity.py +++ b/shade/tests/functional/test_identity.py @@ -79,3 +79,13 @@ class TestIdentity(base.TestCase): role = self.cloud.create_role(role_name) self.assertIsNotNone(role) self.assertTrue(self.cloud.delete_role(role_name)) + + # TODO(Shrews): Once we can support assigning roles within shade, we + # need to make this test a little more specific, and add more for testing + # filtering functionality. + def test_list_role_assignments(self): + if self.cloud.cloud_config.get_api_version('identity') in ('2', '2.0'): + self.skipTest("Identity service does not support role assignments") + assignments = self.cloud.list_role_assignments() + self.assertIsInstance(assignments, list) + self.assertTrue(len(assignments) > 0) diff --git a/shade/tests/unit/test_identity_roles.py b/shade/tests/unit/test_identity_roles.py index 4490d6e51..dad7a1678 100644 --- a/shade/tests/unit/test_identity_roles.py +++ b/shade/tests/unit/test_identity_roles.py @@ -12,13 +12,31 @@ # limitations under the License. import mock +import testtools import shade from shade import meta +from shade import _utils from shade.tests.unit import base from shade.tests import fakes +RAW_ROLE_ASSIGNMENTS = [ + { + "links": {"assignment": "http://example"}, + "role": {"id": "123456"}, + "scope": {"domain": {"id": "161718"}}, + "user": {"id": "313233"} + }, + { + "links": {"assignment": "http://example"}, + "group": {"id": "101112"}, + "role": {"id": "123456"}, + "scope": {"project": {"id": "456789"}} + } +] + + class TestIdentityRoles(base.TestCase): def setUp(self): @@ -63,3 +81,28 @@ class TestIdentityRoles(base.TestCase): mock_get.return_value = meta.obj_to_dict(role_obj) self.assertTrue(self.cloud.delete_role('1234')) self.assertTrue(mock_keystone.roles.delete.called) + + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_list_role_assignments(self, mock_keystone): + mock_keystone.role_assignments.list.return_value = RAW_ROLE_ASSIGNMENTS + ret = self.cloud.list_role_assignments() + mock_keystone.role_assignments.list.assert_called_once_with() + normalized_assignments = _utils.normalize_role_assignments( + RAW_ROLE_ASSIGNMENTS + ) + self.assertEqual(normalized_assignments, ret) + + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_list_role_assignments_filters(self, mock_keystone): + params = dict(user='123', domain='456', effective=True) + self.cloud.list_role_assignments(filters=params) + mock_keystone.role_assignments.list.assert_called_once_with(**params) + + @mock.patch.object(shade.OpenStackCloud, 'keystone_client') + def test_list_role_assignments_exception(self, mock_keystone): + mock_keystone.role_assignments.list.side_effect = Exception() + with testtools.ExpectedException( + shade.OpenStackCloudException, + "Failed to list role assignments" + ): + self.cloud.list_role_assignments()