From 84316f2884f555f0933e3d2923625a393e402a77 Mon Sep 17 00:00:00 2001
From: Tomas Kopecek <tkopecek@redhat.com>
Date: Sep 13 2021 08:31:50 +0000
Subject: [PATCH 1/3] Honour --force-auth for anonymous commands


Fixes: https://pagure.io/koji/issue/2657

---

diff --git a/cli/koji_cli/commands.py b/cli/koji_cli/commands.py
index 95f30aa..a6fedbd 100644
--- a/cli/koji_cli/commands.py
+++ b/cli/koji_cli/commands.py
@@ -1793,6 +1793,7 @@ def handle_prune_signed_copies(goptions, session, args):
     cutoff_ts = time.time() - options.days * 24 * 3600
     if options.debug:
         print("Cutoff date: %s" % time.asctime(time.localtime(cutoff_ts)))
+    activate_session(session, goptions)
     if not options.build:
         if options.verbose:
             print("Getting builds...")
@@ -2145,11 +2146,12 @@ def handle_add_volume(goptions, session, args):
     print("Added volume %(name)s with id %(id)i" % volinfo)
 
 
-def handle_list_volumes(options, session, args):
+def anon_handle_list_volumes(options, session, args):
     "[info] List storage volumes"
     usage = _("usage: %prog list-volumes")
     parser = OptionParser(usage=get_usage_str(usage))
     (options, args) = parser.parse_args(args)
+    ensure_connection(session, options)
     for volinfo in session.listVolumes():
         print(volinfo['name'])
 
@@ -7265,6 +7267,7 @@ def anon_handle_download_logs(options, session, args):
             for child_task in child_tasks:
                 save_logs(child_task['id'], match, task_log_dir, recurse)
 
+    ensure_connection(session, options)
     for arg in args:
         if suboptions.nvr:
             suboptions.recurse = True
@@ -7413,6 +7416,7 @@ def anon_handle_wait_repo(options, session, args):
 
     tag = args[0]
 
+    ensure_connection(session, options)
     if suboptions.target:
         target_info = session.getBuildTarget(tag)
         if not target_info:
@@ -7707,6 +7711,7 @@ def anon_handle_search(options, session, args):
         matchType = 'regexp'
     elif options.exact:
         matchType = 'exact'
+    ensure_connection(session, options)
     data = session.search(pattern, type, matchType)
     for row in data:
         print(row['name'])
diff --git a/cli/koji_cli/lib.py b/cli/koji_cli/lib.py
index f1ab931..bb80ca7 100644
--- a/cli/koji_cli/lib.py
+++ b/cli/koji_cli/lib.py
@@ -136,6 +136,10 @@ def get_usage_str(usage):
 
 
 def ensure_connection(session, options=None):
+    if options and options.force_auth:
+        # in such case we should act as a real activate_session
+        activate_session(session, options)
+        return
     try:
         ret = session.getAPIVersion()
     except requests.exceptions.ConnectionError as ex:
@@ -751,6 +755,8 @@ def activate_session(session, options):
             warn(_("Could not connect to Kerberos authentication service: %s") % e.args[1])
     if not noauth and not session.logged_in:
         error(_("Unable to log in, no authentication methods available"))
+    # don't add "options" to ensure_connection it would create loop in case of --force-auth
+    # when it calls activate_session
     ensure_connection(session)
     if getattr(options, 'debug', None):
         print("successfully connected to hub")
diff --git a/tests/test_cli/test_download_logs.py b/tests/test_cli/test_download_logs.py
index 02e83f3..e628ff5 100644
--- a/tests/test_cli/test_download_logs.py
+++ b/tests/test_cli/test_download_logs.py
@@ -24,7 +24,7 @@ class TestDownloadLogs(utils.CliTestCase):
             'koji_cli.commands.list_task_output_all_volumes').start()
         self.ensuredir = mock.patch('koji.ensuredir').start()
         self.download_file = mock.patch('koji_cli.commands.download_file').start()
-        self.activate_session = mock.patch('koji_cli.commands.activate_session').start()
+        self.ensure_connection = mock.patch('koji_cli.commands.ensure_connection').start()
         self.stdout = mock.patch('sys.stdout', new_callable=six.StringIO).start()
         self.stderr = mock.patch('sys.stderr', new_callable=six.StringIO).start()
 
diff --git a/tests/test_cli/test_list_targets.py b/tests/test_cli/test_list_targets.py
index 5c51993..be8a0bc 100644
--- a/tests/test_cli/test_list_targets.py
+++ b/tests/test_cli/test_list_targets.py
@@ -37,7 +37,7 @@ class TestCliListTargets(utils.CliTestCase):
         time.tzset()
 
     @mock.patch('sys.stderr', new_callable=six.StringIO)
-    @mock.patch('koji_cli.commands.activate_session')
+    @mock.patch('koji_cli.commands.ensure_connection')
     def test_list_targets_error_args(self, ensure_connection_mock, stderr):
         session = mock.MagicMock(getAPIVersion=lambda: koji.API_VERSION,
                                  getBuildTargets=lambda n: [])
@@ -47,7 +47,7 @@ class TestCliListTargets(utils.CliTestCase):
         self.assertExitCode(ex, 2)
 
     @mock.patch('sys.stderr', new_callable=six.StringIO)
-    @mock.patch('koji_cli.commands.activate_session')
+    @mock.patch('koji_cli.commands.ensure_connection')
     def test_list_targets_error_all_not_found(self, ensure_connection_mock, stderr):
         session = mock.MagicMock(getAPIVersion=lambda: koji.API_VERSION,
                                  getBuildTargets=lambda n: [])
@@ -60,7 +60,7 @@ class TestCliListTargets(utils.CliTestCase):
     @mock.patch('optparse.OptionParser.parse_args',
                 return_value=(Values({'quiet': False, 'name': 'f50'}), []))
     @mock.patch('sys.stderr', new_callable=six.StringIO)
-    @mock.patch('koji_cli.commands.activate_session')
+    @mock.patch('koji_cli.commands.ensure_connection')
     def test_list_targets_error_name_not_found(self, ensure_connection_mock, stderr, opt):
         session = mock.MagicMock(getAPIVersion=lambda: koji.API_VERSION,
                                  getBuildTargets=lambda n: [])
@@ -71,7 +71,7 @@ class TestCliListTargets(utils.CliTestCase):
         self.assertTrue('No such build target:' in stderr.getvalue())
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
-    @mock.patch('koji_cli.commands.activate_session')
+    @mock.patch('koji_cli.commands.ensure_connection')
     def test_list_targets_all(self, ensure_connection_mock, stdout):
         session = mock.MagicMock(getAPIVersion=lambda: koji.API_VERSION,
                                  getBuildTargets=lambda n: _mock_targets)
@@ -88,7 +88,7 @@ class TestCliListTargets(utils.CliTestCase):
     @mock.patch('optparse.OptionParser.parse_args',
                 return_value=(Values({'quiet': False, 'name': 'f50'}), []))
     @mock.patch('sys.stdout', new_callable=six.StringIO)
-    @mock.patch('koji_cli.commands.activate_session')
+    @mock.patch('koji_cli.commands.ensure_connection')
     def test_list_targets_one(self, ensure_connection_mock, stdout, opt):
         session = mock.MagicMock(getAPIVersion=lambda: koji.API_VERSION,
                                  getBuildTargets=lambda n: _mock_targets)
diff --git a/tests/test_cli/test_list_volumes.py b/tests/test_cli/test_list_volumes.py
index 4c60bb5..890eba2 100644
--- a/tests/test_cli/test_list_volumes.py
+++ b/tests/test_cli/test_list_volumes.py
@@ -3,7 +3,7 @@ import mock
 import six
 import unittest
 
-from koji_cli.commands import handle_list_volumes
+from koji_cli.commands import anon_handle_list_volumes
 from . import utils
 
 
@@ -13,12 +13,12 @@ class TestListVolumes(utils.CliTestCase):
     maxDiff = None
 
     @mock.patch('sys.stdout', new_callable=six.StringIO)
-    @mock.patch('koji_cli.commands.activate_session')
-    def test_handle_list_volumes(
+    @mock.patch('koji_cli.commands.ensure_connection')
+    def test_anon_handle_list_volumes(
             self,
-            activate_session_mock,
+            ensure_connection_mock,
             stdout):
-        """Test handle_list_volumes function"""
+        """Test anon_handle_list_volumes function"""
         session = mock.MagicMock()
         options = mock.MagicMock()
         vol_info = [
@@ -29,12 +29,12 @@ class TestListVolumes(utils.CliTestCase):
 
         expected = "\n".join([v['name'] for v in vol_info]) + "\n"
         session.listVolumes.return_value = vol_info
-        handle_list_volumes(options, session, [])
+        anon_handle_list_volumes(options, session, [])
         self.assert_console_message(stdout, expected)
 
-    def test_handle_list_volumes_help(self):
+    def test_anon_handle_list_volumes_help(self):
         self.assert_help(
-            handle_list_volumes,
+            anon_handle_list_volumes,
             """Usage: %s list-volumes
 (Specify the --help global option for a list of other help options)
 
diff --git a/tests/test_cli/test_search.py b/tests/test_cli/test_search.py
index c76ea81..2996c2a 100644
--- a/tests/test_cli/test_search.py
+++ b/tests/test_cli/test_search.py
@@ -20,10 +20,12 @@ Available search types: package, build, tag, target, user, host, rpm, maven, win
 %s: error: {message}
 """ % (self.progname, self.progname)
 
+    @mock.patch('koji_cli.commands.ensure_connection')
     @mock.patch('sys.stdout', new_callable=six.StringIO)
     def test_anon_handle_search(
             self,
-            stdout):
+            stdout,
+            ensure_connection_mock):
         """Test anon_handle_search function"""
         session = mock.MagicMock()
         options = mock.MagicMock()
diff --git a/tests/test_cli/test_wait_repo.py b/tests/test_cli/test_wait_repo.py
index 7d4f152..25fe82b 100644
--- a/tests/test_cli/test_wait_repo.py
+++ b/tests/test_cli/test_wait_repo.py
@@ -54,6 +54,7 @@ class TestWaitRepo(utils.CliTestCase):
 
     def setUpMocks(self):
         self.activate_session = mock.patch('koji_cli.commands.activate_session').start()
+        self.ensure_connection = mock.patch('koji_cli.commands.ensure_connection').start()
         self.checkForBuilds = mock.patch('koji.util.checkForBuilds').start()
 
     def tearDown(self):

From eb3f9893b31eb0ce65fccf609b33363f2027de31 Mon Sep 17 00:00:00 2001
From: Tomas Kopecek <tkopecek@redhat.com>
Date: Sep 13 2021 08:31:50 +0000
Subject: [PATCH 2/3] expose force_auth in config


---

diff --git a/cli/koji.conf b/cli/koji.conf
index c1d50dc..331751d 100644
--- a/cli/koji.conf
+++ b/cli/koji.conf
@@ -45,3 +45,6 @@
 
 ;timeout of GSSAPI/SSL authentication by seconds, default: 60
 ;auth_timeout = 60
+
+;enforcing CLI authentication even for anonymous calls
+;force_auth = False
diff --git a/koji/__init__.py b/koji/__init__.py
index 360ab11..d0b7232 100644
--- a/koji/__init__.py
+++ b/koji/__init__.py
@@ -1942,6 +1942,7 @@ def read_config(profile_name, user_config=None):
         'debug_xmlrpc': False,
         'pyver': None,
         'plugin_paths': None,
+        'force_auth': False,
     }
 
     result = config_defaults.copy()
@@ -1978,7 +1979,7 @@ def read_config(profile_name, user_config=None):
             # not have a default value set in the option parser.
             if name in result:
                 if name in ('anon_retry', 'offline_retry', 'use_fast_upload',
-                            'debug', 'debug_xmlrpc'):
+                            'debug', 'debug_xmlrpc', 'force_auth'):
                     result[name] = config.getboolean(profile_name, name)
                 elif name in ('max_retries', 'retry_interval',
                               'offline_retry_interval', 'poll_interval',

From cd554a218e65f91e44ae61e359ff61f28ccb829f Mon Sep 17 00:00:00 2001
From: Tomas Kopecek <tkopecek@redhat.com>
Date: Sep 13 2021 08:31:50 +0000
Subject: [PATCH 3/3] fix option propagation


---

diff --git a/cli/koji b/cli/koji
index 3ac3bc9..dec944b 100755
--- a/cli/koji
+++ b/cli/koji
@@ -127,7 +127,7 @@ def get_options():
     parser.add_option("--password", help=_("specify password"))
     parser.add_option("--noauth", action="store_true", default=False,
                       help=_("do not authenticate"))
-    parser.add_option("--force-auth", action="store_true", default=False,
+    parser.add_option("--force-auth", action="store_true",  # default (False) comes from the config
                       help=_("authenticate even for read-only operations"))
     parser.add_option("--authtype", help=_("force use of a type of authentication, options: "
                                            "noauth, ssl, password, or kerberos"))
diff --git a/cli/koji_cli/commands.py b/cli/koji_cli/commands.py
index a6fedbd..7e4df66 100644
--- a/cli/koji_cli/commands.py
+++ b/cli/koji_cli/commands.py
@@ -2146,12 +2146,12 @@ def handle_add_volume(goptions, session, args):
     print("Added volume %(name)s with id %(id)i" % volinfo)
 
 
-def anon_handle_list_volumes(options, session, args):
+def anon_handle_list_volumes(goptions, session, args):
     "[info] List storage volumes"
     usage = _("usage: %prog list-volumes")
     parser = OptionParser(usage=get_usage_str(usage))
     (options, args) = parser.parse_args(args)
-    ensure_connection(session, options)
+    ensure_connection(session, goptions)
     for volinfo in session.listVolumes():
         print(volinfo['name'])
 
@@ -7690,7 +7690,7 @@ _search_types = ('package', 'build', 'tag', 'target', 'user', 'host', 'rpm',
                  'maven', 'win')
 
 
-def anon_handle_search(options, session, args):
+def anon_handle_search(goptions, session, args):
     "[search] Search the system"
     usage = _("usage: %prog search [options] <search_type> <pattern>")
     usage += _('\nAvailable search types: %s') % ', '.join(_search_types)
@@ -7711,7 +7711,7 @@ def anon_handle_search(options, session, args):
         matchType = 'regexp'
     elif options.exact:
         matchType = 'exact'
-    ensure_connection(session, options)
+    ensure_connection(session, goptions)
     data = session.search(pattern, type, matchType)
     for row in data:
         print(row['name'])

