From 0ff28d5251f9e25eafdc628e29b093b7c694ea48 Mon Sep 17 00:00:00 2001
From: Steve Martinelli <stevemar@ca.ibm.com>
Date: Tue, 16 Dec 2014 15:16:41 -0500
Subject: [PATCH] Allow user list to filter by project

Adds a --project filter to `os user list`, which really
calls the role assignment manager behind the scenes.

Change-Id: I57a75018f12ed3acdf8f6611b6b58bd974f91da2
Closes-Bug: #1397251
---
 doc/source/command-objects/user.rst           | 18 +++----
 openstackclient/identity/v3/user.py           | 53 ++++++++++++++++---
 .../tests/identity/v3/test_user.py            | 47 ++++++++++++++++
 3 files changed, 101 insertions(+), 17 deletions(-)

diff --git a/doc/source/command-objects/user.rst b/doc/source/command-objects/user.rst
index e54c65673c..a9a98fe18f 100644
--- a/doc/source/command-objects/user.rst
+++ b/doc/source/command-objects/user.rst
@@ -101,28 +101,26 @@ List users
 .. code:: bash
 
     os user list
-        [--domain <domain>]
         [--project <project>]
-        [--group <group>]
+        [--domain <domain>]
+        [--group <group> | --project <project>]
         [--long]
 
-.. option:: --domain <domain>
-
-    Filter users by `<domain>` (name or ID)
-
-    .. versionadded:: 3
-
 .. option:: --project <project>
 
     Filter users by `<project>` (name or ID)
 
-    *Removed in version 3.*
+.. option:: --domain <domain>
+
+    Filter users by `<domain>` (name or ID)
+
+    *Identity version 3 only*
 
 .. option:: --group <group>
 
     Filter users by `<group>` membership (name or ID)
 
-    .. versionadded:: 3
+    *Identity version 3 only*
 
 .. option:: --long
 
diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py
index a60c8c83fa..4fb7b6d1e0 100644
--- a/openstackclient/identity/v3/user.py
+++ b/openstackclient/identity/v3/user.py
@@ -188,11 +188,17 @@ class ListUser(lister.Lister):
             metavar='<domain>',
             help='Filter users by <domain> (name or ID)',
         )
-        parser.add_argument(
+        project_or_group = parser.add_mutually_exclusive_group()
+        project_or_group.add_argument(
             '--group',
             metavar='<group>',
             help='Filter users by <group> membership (name or ID)',
         )
+        project_or_group.add_argument(
+            '--project',
+            metavar='<project>',
+            help='Filter users by <project> (name or ID)',
+        )
         parser.add_argument(
             '--long',
             action='store_true',
@@ -219,7 +225,44 @@ class ListUser(lister.Lister):
         else:
             group = None
 
-        # List users
+        if parsed_args.project:
+            if domain is not None:
+                project = utils.find_resource(
+                    identity_client.projects,
+                    parsed_args.project,
+                    domain_id=domain
+                ).id
+            else:
+                project = utils.find_resource(
+                    identity_client.projects,
+                    parsed_args.project,
+                ).id
+
+            assignments = identity_client.role_assignments.list(
+                project=project)
+
+            # NOTE(stevemar): If a user has more than one role on a project
+            # then they will have two entries in the returned data. Since we
+            # are looking for any role, let's just track unique user IDs.
+            user_ids = set()
+            for assignment in assignments:
+                if hasattr(assignment, 'user'):
+                    user_ids.add(assignment.user['id'])
+
+            # NOTE(stevemar): Call find_resource once we have unique IDs, so
+            # it's fewer trips to the Identity API, then collect the data.
+            data = []
+            for user_id in user_ids:
+                user = utils.find_resource(identity_client.users, user_id)
+                data.append(user)
+
+        else:
+            data = identity_client.users.list(
+                domain=domain,
+                group=group,
+            )
+
+        # Column handling
         if parsed_args.long:
             columns = ['ID', 'Name', 'Default Project Id', 'Domain Id',
                        'Description', 'Email', 'Enabled']
@@ -228,11 +271,7 @@ class ListUser(lister.Lister):
             column_headers[3] = 'Domain'
         else:
             columns = ['ID', 'Name']
-            column_headers = copy.deepcopy(columns)
-        data = identity_client.users.list(
-            domain=domain,
-            group=group,
-        )
+            column_headers = columns
 
         return (
             column_headers,
diff --git a/openstackclient/tests/identity/v3/test_user.py b/openstackclient/tests/identity/v3/test_user.py
index dd517e19f2..35dd98ee9e 100644
--- a/openstackclient/tests/identity/v3/test_user.py
+++ b/openstackclient/tests/identity/v3/test_user.py
@@ -44,6 +44,11 @@ class TestUser(identity_fakes.TestIdentityv3):
         self.users_mock = self.app.client_manager.identity.users
         self.users_mock.reset_mock()
 
+        # Shortcut for RoleAssignmentManager Mock
+        self.role_assignments_mock = self.app.client_manager.identity.\
+            role_assignments
+        self.role_assignments_mock.reset_mock()
+
 
 class TestUserCreate(TestUser):
 
@@ -511,6 +516,21 @@ class TestUserList(TestUser):
             loaded=True,
         )
 
+        self.projects_mock.get.return_value = fakes.FakeResource(
+            None,
+            copy.deepcopy(identity_fakes.PROJECT),
+            loaded=True,
+        )
+
+        self.role_assignments_mock.list.return_value = [
+            fakes.FakeResource(
+                None,
+                copy.deepcopy(
+                    identity_fakes.ASSIGNMENT_WITH_PROJECT_ID_AND_USER_ID),
+                loaded=True,
+            )
+        ]
+
         # Get the command object to test
         self.cmd = user.ListUser(self.app, None)
 
@@ -643,6 +663,33 @@ class TestUserList(TestUser):
         ), )
         self.assertEqual(datalist, tuple(data))
 
+    def test_user_list_project(self):
+        arglist = [
+            '--project', identity_fakes.project_name,
+        ]
+        verifylist = [
+            ('project', identity_fakes.project_name),
+        ]
+        parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+        # DisplayCommandBase.take_action() returns two tuples
+        columns, data = self.cmd.take_action(parsed_args)
+
+        kwargs = {
+            'project': identity_fakes.project_id,
+        }
+
+        self.role_assignments_mock.list.assert_called_with(**kwargs)
+        self.users_mock.get.assert_called_with(identity_fakes.user_id)
+
+        collist = ['ID', 'Name']
+        self.assertEqual(columns, collist)
+        datalist = ((
+            identity_fakes.user_id,
+            identity_fakes.user_name,
+        ), )
+        self.assertEqual(datalist, tuple(data))
+
 
 class TestUserSet(TestUser):