From 64e4520b2a79b9046a791f5e3729f5cbfc2d3fa5 Mon Sep 17 00:00:00 2001
From: Nicolas Belouin <nicolas.belouin@gandi.net>
Date: Fri, 14 Jan 2022 15:44:11 +0100
Subject: [PATCH] Add trustor and trustee filtering to trusts list

The keystone API supports filtering trusts by trustor and/or
trustee.

Also adds a shortcut parameter to get trusts with current
user as trustee or trustor.

Signed-off-by: Nicolas Belouin <nicolas.belouin@gandi.net>
Change-Id: I00ed2b68cf8ada214a59f640f4f0a5c9dbc37063
---
 openstackclient/identity/v3/trust.py          |  87 +++++++++++++-
 .../tests/unit/identity/v3/test_trust.py      | 108 +++++++++++++++++-
 ...sing-trust-list-opts-500fd1e4c14e1504.yaml |   9 ++
 3 files changed, 202 insertions(+), 2 deletions(-)
 create mode 100644 releasenotes/notes/add-missing-trust-list-opts-500fd1e4c14e1504.yaml

diff --git a/openstackclient/identity/v3/trust.py b/openstackclient/identity/v3/trust.py
index cd3a65d0da..61273f410d 100644
--- a/openstackclient/identity/v3/trust.py
+++ b/openstackclient/identity/v3/trust.py
@@ -176,10 +176,95 @@ class DeleteTrust(command.Command):
 class ListTrust(command.Lister):
     _description = _("List trusts")
 
+    def get_parser(self, prog_name):
+        parser = super().get_parser(prog_name)
+        parser.add_argument(
+            '--trustor',
+            metavar='<trustor-user>',
+            help=_('Trustor user to filter (name or ID)'),
+        )
+        parser.add_argument(
+            '--trustee',
+            metavar='<trustee-user>',
+            help=_('Trustee user to filter (name or ID)'),
+        )
+        parser.add_argument(
+            '--trustor-domain',
+            metavar='<trustor-domain>',
+            help=_('Domain that contains <trustor> (name or ID)'),
+        )
+        parser.add_argument(
+            '--trustee-domain',
+            metavar='<trustee-domain>',
+            help=_('Domain that contains <trustee> (name or ID)'),
+        )
+        parser.add_argument(
+            '--auth-user',
+            action="store_true",
+            dest='authuser',
+            help=_('Only list trusts related to the authenticated user'),
+        )
+        return parser
+
     def take_action(self, parsed_args):
+        identity_client = self.app.client_manager.identity
+        auth_ref = self.app.client_manager.auth_ref
+
+        if parsed_args.authuser and any([
+            parsed_args.trustor,
+            parsed_args.trustor_domain,
+            parsed_args.trustee,
+            parsed_args.trustee_domain,
+        ]):
+            msg = _("--authuser cannot be used with --trustee or --trustor")
+            raise exceptions.CommandError(msg)
+
+        if parsed_args.trustee_domain and not parsed_args.trustee:
+            msg = _("Using --trustee-domain mandates the use of --trustee")
+            raise exceptions.CommandError(msg)
+
+        if parsed_args.trustor_domain and not parsed_args.trustor:
+            msg = _("Using --trustor-domain mandates the use of --trustor")
+            raise exceptions.CommandError(msg)
+
+        if parsed_args.authuser:
+            if auth_ref:
+                user = common.find_user(
+                    identity_client,
+                    auth_ref.user_id
+                )
+                # We need two calls here as we want trusts with
+                # either the trustor or the trustee set to current user
+                # using a single call would give us trusts with both
+                # trustee and trustor set to current user
+                data1 = identity_client.trusts.list(trustor_user=user)
+                data2 = identity_client.trusts.list(trustee_user=user)
+                data = set(data1 + data2)
+        else:
+            trustor = None
+            if parsed_args.trustor:
+                trustor = common.find_user(
+                    identity_client,
+                    parsed_args.trustor,
+                    parsed_args.trustor_domain,
+                )
+
+            trustee = None
+            if parsed_args.trustee:
+                trustee = common.find_user(
+                    identity_client,
+                    parsed_args.trustor,
+                    parsed_args.trustor_domain,
+                )
+
+            data = self.app.client_manager.identity.trusts.list(
+                trustor_user=trustor,
+                trustee_user=trustee,
+            )
+
         columns = ('ID', 'Expires At', 'Impersonation', 'Project ID',
                    'Trustee User ID', 'Trustor User ID')
-        data = self.app.client_manager.identity.trusts.list()
+
         return (columns,
                 (utils.get_item_properties(
                     s, columns,
diff --git a/openstackclient/tests/unit/identity/v3/test_trust.py b/openstackclient/tests/unit/identity/v3/test_trust.py
index d8cfc59fe8..d530adf5a5 100644
--- a/openstackclient/tests/unit/identity/v3/test_trust.py
+++ b/openstackclient/tests/unit/identity/v3/test_trust.py
@@ -206,7 +206,113 @@ class TestTrustList(TestTrust):
         # containing the data to be listed.
         columns, data = self.cmd.take_action(parsed_args)
 
-        self.trusts_mock.list.assert_called_with()
+        self.trusts_mock.list.assert_called_with(
+            trustor_user=None,
+            trustee_user=None,
+        )
+
+        collist = ('ID', 'Expires At', 'Impersonation', 'Project ID',
+                   'Trustee User ID', 'Trustor User ID')
+        self.assertEqual(collist, columns)
+        datalist = ((
+            identity_fakes.trust_id,
+            identity_fakes.trust_expires,
+            identity_fakes.trust_impersonation,
+            identity_fakes.project_id,
+            identity_fakes.user_id,
+            identity_fakes.user_id
+        ), )
+        self.assertEqual(datalist, tuple(data))
+
+    def test_trust_list_auth_user(self):
+        auth_ref = self.app.client_manager.auth_ref = mock.Mock()
+        auth_ref.user_id.return_value = identity_fakes.user_id
+
+        arglist = ['--auth-user']
+        verifylist = [
+            ('trustor', None),
+            ('trustee', None),
+            ('authuser', 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.trusts_mock.list.assert_any_call(
+            trustor_user=self.users_mock.get()
+        )
+        self.trusts_mock.list.assert_any_call(
+            trustee_user=self.users_mock.get()
+        )
+
+        collist = ('ID', 'Expires At', 'Impersonation', 'Project ID',
+                   'Trustee User ID', 'Trustor User ID')
+        self.assertEqual(collist, columns)
+        datalist = ((
+            identity_fakes.trust_id,
+            identity_fakes.trust_expires,
+            identity_fakes.trust_impersonation,
+            identity_fakes.project_id,
+            identity_fakes.user_id,
+            identity_fakes.user_id
+        ), )
+        self.assertEqual(datalist, tuple(data))
+
+    def test_trust_list_trustee(self):
+        arglist = ['--trustee', identity_fakes.user_name]
+        verifylist = [
+            ('trustor', None),
+            ('trustee', identity_fakes.user_name),
+            ('authuser', False),
+        ]
+        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)
+
+        print(self.trusts_mock.list.call_args_list)
+        self.trusts_mock.list.assert_any_call(
+            trustee_user=self.users_mock.get(),
+            trustor_user=None,
+        )
+
+        collist = ('ID', 'Expires At', 'Impersonation', 'Project ID',
+                   'Trustee User ID', 'Trustor User ID')
+        self.assertEqual(collist, columns)
+        datalist = ((
+            identity_fakes.trust_id,
+            identity_fakes.trust_expires,
+            identity_fakes.trust_impersonation,
+            identity_fakes.project_id,
+            identity_fakes.user_id,
+            identity_fakes.user_id
+        ), )
+        self.assertEqual(datalist, tuple(data))
+
+    def test_trust_list_trustor(self):
+        arglist = ['--trustor', identity_fakes.user_name]
+        verifylist = [
+            ('trustee', None),
+            ('trustor', identity_fakes.user_name),
+            ('authuser', False),
+        ]
+        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)
+
+        print(self.trusts_mock.list.call_args_list)
+        self.trusts_mock.list.assert_any_call(
+            trustor_user=self.users_mock.get(),
+            trustee_user=None,
+        )
 
         collist = ('ID', 'Expires At', 'Impersonation', 'Project ID',
                    'Trustee User ID', 'Trustor User ID')
diff --git a/releasenotes/notes/add-missing-trust-list-opts-500fd1e4c14e1504.yaml b/releasenotes/notes/add-missing-trust-list-opts-500fd1e4c14e1504.yaml
new file mode 100644
index 0000000000..8918dd8777
--- /dev/null
+++ b/releasenotes/notes/add-missing-trust-list-opts-500fd1e4c14e1504.yaml
@@ -0,0 +1,9 @@
+---
+features:
+  - |
+    Add missing ``--trustee``, ``--trustee-domain``, ``--trustor``,
+    ``--trustor-domain`` options to ``trust list`` command, to allow
+    filtering trusts by trustee and trustor.
+  - |
+    Add ``--authuser`` option to ``trust list`` command, to allow
+    displaying only trusts related to current authenticated user