Skip to content

Commit f38d6e7

Browse files
Jenkinsopenstack-gerrit
authored andcommitted
Merge "Allow user list to filter by project"
2 parents b7a130f + 0ff28d5 commit f38d6e7

3 files changed

Lines changed: 101 additions & 17 deletions

File tree

‎doc/source/command-objects/user.rst‎

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -101,28 +101,26 @@ List users
101101
.. code:: bash
102102
103103
os user list
104-
[--domain <domain>]
105104
[--project <project>]
106-
[--group <group>]
105+
[--domain <domain>]
106+
[--group <group> | --project <project>]
107107
[--long]
108108
109-
.. option:: --domain <domain>
110-
111-
Filter users by `<domain>` (name or ID)
112-
113-
.. versionadded:: 3
114-
115109
.. option:: --project <project>
116110

117111
Filter users by `<project>` (name or ID)
118112

119-
*Removed in version 3.*
113+
.. option:: --domain <domain>
114+
115+
Filter users by `<domain>` (name or ID)
116+
117+
*Identity version 3 only*
120118

121119
.. option:: --group <group>
122120

123121
Filter users by `<group>` membership (name or ID)
124122

125-
.. versionadded:: 3
123+
*Identity version 3 only*
126124

127125
.. option:: --long
128126

‎openstackclient/identity/v3/user.py‎

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -188,11 +188,17 @@ def get_parser(self, prog_name):
188188
metavar='<domain>',
189189
help='Filter users by <domain> (name or ID)',
190190
)
191-
parser.add_argument(
191+
project_or_group = parser.add_mutually_exclusive_group()
192+
project_or_group.add_argument(
192193
'--group',
193194
metavar='<group>',
194195
help='Filter users by <group> membership (name or ID)',
195196
)
197+
project_or_group.add_argument(
198+
'--project',
199+
metavar='<project>',
200+
help='Filter users by <project> (name or ID)',
201+
)
196202
parser.add_argument(
197203
'--long',
198204
action='store_true',
@@ -219,7 +225,44 @@ def take_action(self, parsed_args):
219225
else:
220226
group = None
221227

222-
# List users
228+
if parsed_args.project:
229+
if domain is not None:
230+
project = utils.find_resource(
231+
identity_client.projects,
232+
parsed_args.project,
233+
domain_id=domain
234+
).id
235+
else:
236+
project = utils.find_resource(
237+
identity_client.projects,
238+
parsed_args.project,
239+
).id
240+
241+
assignments = identity_client.role_assignments.list(
242+
project=project)
243+
244+
# NOTE(stevemar): If a user has more than one role on a project
245+
# then they will have two entries in the returned data. Since we
246+
# are looking for any role, let's just track unique user IDs.
247+
user_ids = set()
248+
for assignment in assignments:
249+
if hasattr(assignment, 'user'):
250+
user_ids.add(assignment.user['id'])
251+
252+
# NOTE(stevemar): Call find_resource once we have unique IDs, so
253+
# it's fewer trips to the Identity API, then collect the data.
254+
data = []
255+
for user_id in user_ids:
256+
user = utils.find_resource(identity_client.users, user_id)
257+
data.append(user)
258+
259+
else:
260+
data = identity_client.users.list(
261+
domain=domain,
262+
group=group,
263+
)
264+
265+
# Column handling
223266
if parsed_args.long:
224267
columns = ['ID', 'Name', 'Default Project Id', 'Domain Id',
225268
'Description', 'Email', 'Enabled']
@@ -228,11 +271,7 @@ def take_action(self, parsed_args):
228271
column_headers[3] = 'Domain'
229272
else:
230273
columns = ['ID', 'Name']
231-
column_headers = copy.deepcopy(columns)
232-
data = identity_client.users.list(
233-
domain=domain,
234-
group=group,
235-
)
274+
column_headers = columns
236275

237276
return (
238277
column_headers,

‎openstackclient/tests/identity/v3/test_user.py‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ def setUp(self):
4444
self.users_mock = self.app.client_manager.identity.users
4545
self.users_mock.reset_mock()
4646

47+
# Shortcut for RoleAssignmentManager Mock
48+
self.role_assignments_mock = self.app.client_manager.identity.\
49+
role_assignments
50+
self.role_assignments_mock.reset_mock()
51+
4752

4853
class TestUserCreate(TestUser):
4954

@@ -511,6 +516,21 @@ def setUp(self):
511516
loaded=True,
512517
)
513518

519+
self.projects_mock.get.return_value = fakes.FakeResource(
520+
None,
521+
copy.deepcopy(identity_fakes.PROJECT),
522+
loaded=True,
523+
)
524+
525+
self.role_assignments_mock.list.return_value = [
526+
fakes.FakeResource(
527+
None,
528+
copy.deepcopy(
529+
identity_fakes.ASSIGNMENT_WITH_PROJECT_ID_AND_USER_ID),
530+
loaded=True,
531+
)
532+
]
533+
514534
# Get the command object to test
515535
self.cmd = user.ListUser(self.app, None)
516536

@@ -643,6 +663,33 @@ def test_user_list_long(self):
643663
), )
644664
self.assertEqual(datalist, tuple(data))
645665

666+
def test_user_list_project(self):
667+
arglist = [
668+
'--project', identity_fakes.project_name,
669+
]
670+
verifylist = [
671+
('project', identity_fakes.project_name),
672+
]
673+
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
674+
675+
# DisplayCommandBase.take_action() returns two tuples
676+
columns, data = self.cmd.take_action(parsed_args)
677+
678+
kwargs = {
679+
'project': identity_fakes.project_id,
680+
}
681+
682+
self.role_assignments_mock.list.assert_called_with(**kwargs)
683+
self.users_mock.get.assert_called_with(identity_fakes.user_id)
684+
685+
collist = ['ID', 'Name']
686+
self.assertEqual(columns, collist)
687+
datalist = ((
688+
identity_fakes.user_id,
689+
identity_fakes.user_name,
690+
), )
691+
self.assertEqual(datalist, tuple(data))
692+
646693

647694
class TestUserSet(TestUser):
648695

0 commit comments

Comments
 (0)