diff --git a/doc/source/command-objects/role-assignment.rst b/doc/source/command-objects/role-assignment.rst index 893ebdc4e9..ef1b22eeeb 100644 --- a/doc/source/command-objects/role-assignment.rst +++ b/doc/source/command-objects/role-assignment.rst @@ -2,7 +2,7 @@ role assignment =============== -Identity v3 +Identity v2, v3 role assignment list -------------------- @@ -23,11 +23,14 @@ List role assignments [--project-domain ] [--effective] [--inherited] + [--names] .. option:: --role Role to filter (name or ID) + .. versionadded:: 3 + .. option:: --user User to filter (name or ID) @@ -37,19 +40,27 @@ List role assignments Domain the user belongs to (name or ID). This can be used in case collisions between user names exist. + .. versionadded:: 3 + .. option:: --group Group to filter (name or ID) + .. versionadded:: 3 + .. option:: --group-domain Domain the group belongs to (name or ID). This can be used in case collisions between group names exist. + .. versionadded:: 3 + .. option:: --domain Domain to filter (name or ID) + .. versionadded:: 3 + .. option:: --project Project to filter (name or ID) @@ -59,14 +70,29 @@ List role assignments Domain the project belongs to (name or ID). This can be used in case collisions between project names exist. + .. versionadded:: 3 + .. option:: --effective Returns only effective role assignments (defaults to False) + .. versionadded:: 3 + .. option:: --inherited Specifies if the role grant is inheritable to the sub projects + .. versionadded:: 3 + .. option:: --names Returns role assignments with names instead of IDs + +.. option:: --auth-user + + Returns role assignments for the authenticated user. + +.. option:: --auth-project + + Returns role assignments for the project to which the authenticated user + is scoped. diff --git a/doc/source/command-objects/role.rst b/doc/source/command-objects/role.rst index 48751ed7bb..5542a35b9c 100644 --- a/doc/source/command-objects/role.rst +++ b/doc/source/command-objects/role.rst @@ -7,7 +7,7 @@ Identity v2, v3 role add -------- -Add role to a user or group in a project or domain +Add role assignment to a user or group in a project or domain .. program:: role add .. code:: bash @@ -123,31 +123,33 @@ List roles Filter roles by (name or ID) - .. versionadded:: 3 + (Deprecated, please use ``role assignment list`` instead) .. option:: --project Filter roles by (name or ID) - .. versionadded:: 3 + (Deprecated, please use ``role assignment list`` instead) .. option:: --user Filter roles by (name or ID) - .. versionadded:: 3 + (Deprecated, please use ``role assignment list`` instead) .. option:: --group Filter roles by (name or ID) - .. versionadded:: 3 + (Deprecated, please use ``role assignment list`` instead) .. option:: --user-domain Domain the user belongs to (name or ID). This can be used in case collisions between user names exist. + (Deprecated, please use ``role assignment list`` instead) + .. versionadded:: 3 .. option:: --group-domain @@ -155,6 +157,8 @@ List roles Domain the group belongs to (name or ID). This can be used in case collisions between group names exist. + (Deprecated, please use ``role assignment list`` instead) + .. versionadded:: 3 .. option:: --project-domain @@ -162,18 +166,22 @@ List roles Domain the project belongs to (name or ID). This can be used in case collisions between project names exist. + (Deprecated, please use ``role assignment list`` instead) + .. versionadded:: 3 .. option:: --inherited Specifies if the role grant is inheritable to the sub projects. + (Deprecated, please use ``role assignment list`` instead) + .. versionadded:: 3 role remove ----------- -Remove role from domain/project : user/group +Remove role assignment from domain/project : user/group .. program:: role remove .. code:: bash diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index 191cdaa3d1..b4b67bad29 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -150,6 +150,15 @@ class ListRole(command.Lister): return parser def take_action(self, parsed_args): + + def _deprecated(): + # NOTE(henry-nash): Deprecated as of Newton, so we should remove + # this in the 'P' release. + self.log.warning(_('Listing assignments using role list is ' + 'deprecated as of the Newton release. Use role ' + 'assignment list --user --project ' + ' --names instead.')) + identity_client = self.app.client_manager.identity auth_ref = self.app.client_manager.auth_ref @@ -166,6 +175,7 @@ class ListRole(command.Lister): identity_client.projects, parsed_args.project, ) + _deprecated() data = identity_client.roles.roles_for_user(user.id, project.id) elif parsed_args.user: @@ -181,6 +191,7 @@ class ListRole(command.Lister): else: msg = _("Project must be specified") raise exceptions.CommandError(msg) + _deprecated() data = identity_client.roles.roles_for_user(user.id, project.id) elif parsed_args.project: project = utils.find_resource( @@ -195,6 +206,7 @@ class ListRole(command.Lister): else: msg = _("User must be specified") raise exceptions.CommandError(msg) + _deprecated() data = identity_client.roles.roles_for_user(user.id, project.id) if parsed_args.user or parsed_args.project: @@ -249,6 +261,10 @@ class ListUserRole(command.Lister): msg = _("User must be specified") raise exceptions.CommandError(msg) + self.log.warning(_('Listing assignments using user role list is ' + 'deprecated as of the Newton release. Use role ' + 'assignment list --user --project ' + ' --names instead.')) project = utils.find_resource( identity_client.tenants, parsed_args.project, diff --git a/openstackclient/identity/v2_0/role_assignment.py b/openstackclient/identity/v2_0/role_assignment.py new file mode 100644 index 0000000000..406508ac37 --- /dev/null +++ b/openstackclient/identity/v2_0/role_assignment.py @@ -0,0 +1,113 @@ +# 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. +# + +"""Identity v2 Assignment action implementations """ + +from openstackclient.common import command +from openstackclient.common import exceptions +from openstackclient.common import utils +from openstackclient.i18n import _ # noqa + + +class ListRoleAssignment(command.Lister): + """List role assignments""" + + def get_parser(self, prog_name): + parser = super(ListRoleAssignment, self).get_parser(prog_name) + parser.add_argument( + '--user', + metavar='', + help='User to filter (name or ID)', + ) + parser.add_argument( + '--project', + metavar='', + help='Project to filter (name or ID)', + ) + parser.add_argument( + '--names', + action="store_true", + help='Display names instead of IDs', + ) + parser.add_argument( + '--auth-user', + action="store_true", + dest='authuser', + help='Only list assignments for the authenticated user', + ) + parser.add_argument( + '--auth-project', + action="store_true", + dest='authproject', + help='Only list assignments for the project to which the ' + 'authenticated user\'s token is scoped', + ) + return parser + + def take_action(self, parsed_args): + identity_client = self.app.client_manager.identity + auth_ref = self.app.client_manager.auth_ref + + include_names = True if parsed_args.names else False + + user = None + if parsed_args.user: + user = utils.find_resource( + identity_client.users, + parsed_args.user, + ) + elif parsed_args.authuser: + if auth_ref: + user = utils.find_resource( + identity_client.users, + auth_ref.user_id + ) + + project = None + if parsed_args.project: + project = utils.find_resource( + identity_client.projects, + parsed_args.project, + ) + elif parsed_args.authproject: + if auth_ref: + project = utils.find_resource( + identity_client.projects, + auth_ref.project_id + ) + + # If user or project is not specified, we would ideally list all + # relevant assignments in the system (to be compatible with v3). + # However, there is no easy way of doing that in v2. + if not user or not project: + msg = _("Project and User must be specified") + raise exceptions.CommandError(msg) + else: + data = identity_client.roles.roles_for_user(user.id, project.id) + + columns = ('Role', 'User', 'Project') + for user_role in data: + if include_names: + setattr(user_role, 'role', user_role.name) + user_role.user = user.name + user_role.project = project.name + else: + setattr(user_role, 'role', user_role.id) + user_role.user = user.id + user_role.project = project.id + + return (columns, + (utils.get_item_properties( + s, columns, + formatters={}, + ) for s in data)) diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index 273801799d..e8a03ff3db 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -251,6 +251,10 @@ class ListRole(command.Lister): for user_role in data: user_role.user = user.name user_role.domain = domain.name + self.log.warning(_('Listing assignments using role list is ' + 'deprecated. Use role assignment list --user ' + ' --domain --names ' + 'instead.')) elif parsed_args.user and parsed_args.project: columns = ('ID', 'Name', 'Project', 'User') data = identity_client.roles.list( @@ -261,6 +265,10 @@ class ListRole(command.Lister): for user_role in data: user_role.user = user.name user_role.project = project.name + self.log.warning(_('Listing assignments using role list is ' + 'deprecated. Use role assignment list --user ' + ' --project --names ' + 'instead.')) elif parsed_args.user: columns = ('ID', 'Name') data = identity_client.roles.list( @@ -268,6 +276,10 @@ class ListRole(command.Lister): domain='default', os_inherit_extension_inherited=parsed_args.inherited ) + self.log.warning(_('Listing assignments using role list is ' + 'deprecated. Use role assignment list --user ' + ' --domain default --names ' + 'instead.')) elif parsed_args.group and parsed_args.domain: columns = ('ID', 'Name', 'Domain', 'Group') data = identity_client.roles.list( @@ -278,6 +290,10 @@ class ListRole(command.Lister): for group_role in data: group_role.group = group.name group_role.domain = domain.name + self.log.warning(_('Listing assignments using role list is ' + 'deprecated. Use role assignment list --group ' + ' --domain --names ' + 'instead.')) elif parsed_args.group and parsed_args.project: columns = ('ID', 'Name', 'Project', 'Group') data = identity_client.roles.list( @@ -288,6 +304,10 @@ class ListRole(command.Lister): for group_role in data: group_role.group = group.name group_role.project = project.name + self.log.warning(_('Listing assignments using role list is ' + 'deprecated. Use role assignment list --group ' + ' --project --names ' + 'instead.')) else: sys.stderr.write(_("Error: If a user or group is specified, " "either --domain or --project must also be " diff --git a/openstackclient/identity/v3/role_assignment.py b/openstackclient/identity/v3/role_assignment.py index 39e2336d6f..6177d1a5f6 100644 --- a/openstackclient/identity/v3/role_assignment.py +++ b/openstackclient/identity/v3/role_assignment.py @@ -67,6 +67,19 @@ class ListRoleAssignment(command.Lister): ) common.add_project_domain_option_to_parser(parser) common.add_inherited_option_to_parser(parser) + parser.add_argument( + '--auth-user', + action="store_true", + dest='authuser', + help='Only list assignments for the authenticated user', + ) + parser.add_argument( + '--auth-project', + action="store_true", + dest='authproject', + help='Only list assignments for the project to which the ' + 'authenticated user\'s token is scoped', + ) return parser def _as_tuple(self, assignment): @@ -75,6 +88,7 @@ class ListRoleAssignment(command.Lister): def take_action(self, parsed_args): identity_client = self.app.client_manager.identity + auth_ref = self.app.client_manager.auth_ref role = None if parsed_args.role: @@ -90,6 +104,12 @@ class ListRoleAssignment(command.Lister): parsed_args.user, parsed_args.user_domain, ) + elif parsed_args.authuser: + if auth_ref: + user = common.find_user( + identity_client, + auth_ref.user_id + ) domain = None if parsed_args.domain: @@ -105,6 +125,12 @@ class ListRoleAssignment(command.Lister): parsed_args.project, parsed_args.project_domain, ) + elif parsed_args.authproject: + if auth_ref: + project = common.find_project( + identity_client, + auth_ref.project_id + ) group = None if parsed_args.group: diff --git a/openstackclient/tests/identity/v2_0/fakes.py b/openstackclient/tests/identity/v2_0/fakes.py index 662d56b657..a715427cbb 100644 --- a/openstackclient/tests/identity/v2_0/fakes.py +++ b/openstackclient/tests/identity/v2_0/fakes.py @@ -50,6 +50,11 @@ ROLE = { 'name': role_name, } +ROLE_2 = { + 'id': '2', + 'name': 'bigboss', +} + service_id = '1925-10-11' service_name = 'elmore' service_description = 'Leonard, Elmore, rip' diff --git a/openstackclient/tests/identity/v2_0/test_role_assignment.py b/openstackclient/tests/identity/v2_0/test_role_assignment.py new file mode 100644 index 0000000000..ab48c2f418 --- /dev/null +++ b/openstackclient/tests/identity/v2_0/test_role_assignment.py @@ -0,0 +1,270 @@ +# 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 mock + +from openstackclient.common import exceptions +from openstackclient.identity.v2_0 import role_assignment +from openstackclient.tests import fakes +from openstackclient.tests.identity.v2_0 import fakes as identity_fakes + + +class TestRoleAssignment(identity_fakes.TestIdentityv2): + + def setUp(self): + super(TestRoleAssignment, self).setUp() + + +class TestRoleAssignmentList(TestRoleAssignment): + + columns = ( + 'Role', + 'User', + 'Project', + ) + + def setUp(self): + super(TestRoleAssignment, self).setUp() + + # Get a shortcut to the UserManager Mock + self.users_mock = self.app.client_manager.identity.users + self.users_mock.reset_mock() + + # Get a shortcut to the ProjectManager Mock + self.projects_mock = self.app.client_manager.identity.projects + self.projects_mock.reset_mock() + + # Get a shortcut to the RoleManager Mock + self.roles_mock = self.app.client_manager.identity.roles + self.roles_mock.reset_mock() + + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.PROJECT), + loaded=True, + ) + + self.users_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.USER), + loaded=True, + ) + + self.roles_mock.roles_for_user.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy(identity_fakes.ROLE), + loaded=True, + ), + ] + + # Get the command object to test + self.cmd = role_assignment.ListRoleAssignment(self.app, None) + + def test_role_assignment_list_no_filters(self): + + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # This argument combination should raise a CommandError + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + + def test_role_assignment_list_only_project_filter(self): + + arglist = [ + '--project', identity_fakes.project_name, + ] + verifylist = [ + ('project', identity_fakes.project_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # This argument combination should raise a CommandError + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + + def test_role_assignment_list_only_user_filter(self): + + arglist = [ + '--user', identity_fakes.user_name, + ] + verifylist = [ + ('user', identity_fakes.user_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # This argument combination should raise a CommandError + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + + def test_role_assignment_list_project_and_user(self): + + self.roles_mock.roles_for_user.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy( + identity_fakes.ROLE), + loaded=True, + ), + fakes.FakeResource( + None, + copy.deepcopy( + identity_fakes.ROLE_2), + loaded=True, + ), + ] + + arglist = [ + '--project', identity_fakes.project_name, + '--user', identity_fakes.user_name, + ] + verifylist = [ + ('user', identity_fakes.user_name), + ('project', identity_fakes.project_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) + + self.roles_mock.roles_for_user.assert_called_with( + identity_fakes.user_id, + identity_fakes.project_id, + ) + + self.assertEqual(self.columns, columns) + datalist = (( + identity_fakes.role_id, + identity_fakes.user_id, + identity_fakes.project_id, + ), (identity_fakes.ROLE_2['id'], + identity_fakes.user_id, + identity_fakes.project_id, + ),) + self.assertEqual(datalist, tuple(data)) + + def test_role_assignment_list_def_creds(self): + + auth_ref = self.app.client_manager.auth_ref = mock.MagicMock() + auth_ref.project_id.return_value = identity_fakes.project_id + auth_ref.user_id.return_value = identity_fakes.user_id + + self.roles_mock.roles_for_user.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy( + identity_fakes.ROLE), + loaded=True, + ), + fakes.FakeResource( + None, + copy.deepcopy( + identity_fakes.ROLE_2), + loaded=True, + ), + ] + + arglist = [ + '--auth-user', + '--auth-project', + ] + verifylist = [ + ('authuser', True), + ('authproject', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) + + self.roles_mock.roles_for_user.assert_called_with( + identity_fakes.user_id, + identity_fakes.project_id, + ) + + self.assertEqual(self.columns, columns) + datalist = (( + identity_fakes.role_id, + identity_fakes.user_id, + identity_fakes.project_id, + ), (identity_fakes.ROLE_2['id'], + identity_fakes.user_id, + identity_fakes.project_id, + ),) + self.assertEqual(datalist, tuple(data)) + + def test_role_assignment_list_by_name_project_and_user(self): + + self.roles_mock.roles_for_user.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy( + identity_fakes.ROLE), + loaded=True, + ), + fakes.FakeResource( + None, + copy.deepcopy( + identity_fakes.ROLE_2), + loaded=True, + ), + ] + + arglist = [ + '--project', identity_fakes.project_name, + '--user', identity_fakes.user_name, + '--names' + ] + verifylist = [ + ('user', identity_fakes.user_name), + ('project', identity_fakes.project_name), + ('names', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) + + self.roles_mock.roles_for_user.assert_called_with( + identity_fakes.user_id, + identity_fakes.project_id, + ) + + self.assertEqual(self.columns, columns) + datalist = (( + identity_fakes.role_name, + identity_fakes.user_name, + identity_fakes.project_name, + ), (identity_fakes.ROLE_2['name'], + identity_fakes.user_name, + identity_fakes.project_name, + ),) + self.assertEqual(datalist, tuple(data)) diff --git a/openstackclient/tests/identity/v3/test_role_assignment.py b/openstackclient/tests/identity/v3/test_role_assignment.py index 8956b74fb3..0ae67c72e0 100644 --- a/openstackclient/tests/identity/v3/test_role_assignment.py +++ b/openstackclient/tests/identity/v3/test_role_assignment.py @@ -12,6 +12,7 @@ # import copy +import mock from openstackclient.identity.v3 import role_assignment from openstackclient.tests import fakes @@ -374,6 +375,65 @@ class TestRoleAssignmentList(TestRoleAssignment): ),) self.assertEqual(datalist, tuple(data)) + def test_role_assignment_list_def_creds(self): + + auth_ref = self.app.client_manager.auth_ref = mock.MagicMock() + auth_ref.project_id.return_value = identity_fakes.project_id + auth_ref.user_id.return_value = identity_fakes.user_id + + self.role_assignments_mock.list.return_value = [ + fakes.FakeResource( + None, + copy.deepcopy( + identity_fakes.ASSIGNMENT_WITH_PROJECT_ID_AND_USER_ID), + loaded=True, + ), + ] + + arglist = [ + '--auth-user', + '--auth-project', + ] + verifylist = [ + ('user', None), + ('group', None), + ('domain', None), + ('project', None), + ('role', None), + ('effective', False), + ('inherited', False), + ('names', False), + ('authuser', True), + ('authproject', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) + + self.role_assignments_mock.list.assert_called_with( + domain=None, + user=self.users_mock.get(), + group=None, + project=self.projects_mock.get(), + role=None, + effective=False, + os_inherit_extension_inherited_to=None, + include_names=False) + + self.assertEqual(self.columns, columns) + datalist = (( + identity_fakes.role_id, + identity_fakes.user_id, + '', + identity_fakes.project_id, + '', + False + ),) + self.assertEqual(datalist, tuple(data)) + def test_role_assignment_list_effective(self): self.role_assignments_mock.list.return_value = [ diff --git a/releasenotes/notes/bug-1605774-28aec51f6ec4926e.yaml b/releasenotes/notes/bug-1605774-28aec51f6ec4926e.yaml new file mode 100644 index 0000000000..8c7958f4b9 --- /dev/null +++ b/releasenotes/notes/bug-1605774-28aec51f6ec4926e.yaml @@ -0,0 +1,4 @@ +--- +features: + - Deprecate ``role list`` arguments in favor of ``role assignment`` command. + [Bug `1605774 `_] diff --git a/setup.cfg b/setup.cfg index b8b866108f..f1abceb93d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -172,6 +172,7 @@ openstack.identity.v2 = role_list = openstackclient.identity.v2_0.role:ListRole role_remove = openstackclient.identity.v2_0.role:RemoveRole role_show = openstackclient.identity.v2_0.role:ShowRole + role_assignment_list = openstackclient.identity.v2_0.role_assignment:ListRoleAssignment service_create = openstackclient.identity.v2_0.service:CreateService service_delete = openstackclient.identity.v2_0.service:DeleteService