Repository: fedora-infra/bodhi

Issue:
bodhi-push overrides the kerberos ticket cache when run as root
```bodhi-push``` is sometimes run as root instead of as ```apache```. When this happens, it overwrites ```/tmp/krb5cc_%{uid}``` and makes it only readable by root:

```
-rw-------. 1 root   root   1875 Mar  5 17:53 /tmp/krb5cc_%{uid}
```

Once this state is achieved, the composer fails because it cannot use the ticket cache (since the composer would typically be run as the ```apache``` user.

@puiterwijk pointed out that we don't really need ```bodhi-push``` to use Koji authenticated since it is only performing read queries, and suggested adding a new boolean parameter to ```setup_buildsys()``` to specify whether an authenticated session is needed. ```bodhi-push``` would then use an unauthenticated Koji session, which would then make things OK if a release engineer forgot to run ```bodhi-push``` as apache instead of as root.


Discussion:

Resolving patch:
diff --git a/bodhi/server/buildsys.py b/bodhi/server/buildsys.py
--- a/bodhi/server/buildsys.py
+++ b/bodhi/server/buildsys.py
@@ -473,12 +473,13 @@ def getRPMHeaders(self, rpmID, headers):
             }
 
 
-def koji_login(config):
+def koji_login(config, authenticate):
     """
     Login to Koji and return the session.
 
     Args:
         config (bodhi.server.config.BodhiConfig): Bodhi's configuration dictionary.
+        authenticate (bool): If True, establish an authenticated client session.
     Returns:
         koji.ClientSession: An authenticated Koji ClientSession that is ready to use.
     """
@@ -492,7 +493,7 @@ def koji_login(config):
     }
 
     koji_client = koji.ClientSession(_koji_hub, koji_options)
-    if not koji_client.krb_login(**get_krb_conf(config)):
+    if authenticate and not koji_client.krb_login(**get_krb_conf(config)):
         log.error('Koji krb_login failed')
     return koji_client
 
@@ -545,12 +546,13 @@ def teardown_buildsystem():
     DevBuildsys.clear()
 
 
-def setup_buildsystem(settings):
+def setup_buildsystem(settings, authenticate=True):
     """
     Initialize the buildsystem client.
 
     Args:
         settings (bodhi.server.config.BodhiConfig): Bodhi's config.
+        authenticate (bool): If True, establish an authenticated Koji session. Defaults to True.
     Raises:
         ValueError: If the buildsystem is configured to an invalid value.
     """
@@ -567,7 +569,7 @@ def setup_buildsystem(settings):
 
         def get_koji_login():
             """Call koji_login with settings and return the result."""
-            return koji_login(config=settings)
+            return koji_login(config=settings, authenticate=authenticate)
 
         _buildsystem = get_koji_login
     elif buildsys in ('dev', 'dummy', None):
diff --git a/bodhi/server/push.py b/bodhi/server/push.py
--- a/bodhi/server/push.py
+++ b/bodhi/server/push.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright © 2007-2017 Red Hat, Inc.
+# Copyright © 2007-2018 Red Hat, Inc.
 #
 # This file is part of Bodhi.
 #
@@ -20,7 +20,7 @@
 from sqlalchemy.sql import or_
 import click
 
-from bodhi.server import (initialize_db, get_koji, setup_buildsys)
+from bodhi.server import (buildsys, initialize_db, get_koji)
 from bodhi.server.config import config
 from bodhi.server.models import (Compose, ComposeState, Release, ReleaseState, Build, Update,
                                  UpdateRequest)
@@ -35,7 +35,10 @@ def update_sig_status(update):
     """Update build signature status for builds in update."""
     global _koji
     if _koji is None:
-        setup_buildsys()
+        # We don't want to authenticate to the buildsystem, because this script is often mistakenly
+        # run as root and this can cause the ticket cache to become root owned with 0600 perms,
+        # which will cause the compose to fail when it tries to use it to authenticate to Koji.
+        buildsys.setup_buildsystem(config, authenticate=False)
         _koji = get_koji(None)
     for build in update.builds:
         if not build.signed:


Test patch:
diff --git a/bodhi/tests/server/test_buildsys.py b/bodhi/tests/server/test_buildsys.py
--- a/bodhi/tests/server/test_buildsys.py
+++ b/bodhi/tests/server/test_buildsys.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright © 2016-2017 Red Hat, Inc.
+# Copyright © 2016-2018 Red Hat, Inc.
 #
 # This file is part of Bodhi.
 #
@@ -104,28 +104,57 @@ def test_krb_principal(self):
 
 class TestKojiLogin(unittest.TestCase):
     """This class contains tests for the koji_login() function."""
+
+    @mock.patch.object(buildsys, '_koji_hub', 'http://example.com/koji')
+    @mock.patch('bodhi.server.buildsys.koji.ClientSession.krb_login')
+    @mock.patch('bodhi.server.buildsys.log.error')
+    def test_authenticate_false(self, error, krb_login):
+        """If authenticate is False, no attempt to call krb_login() shold be made."""
+        config = {'some_meaningless_other_key': 'boring_value', 'krb_ccache': 'a_ccache',
+                  'krb_keytab': 'a_keytab', 'krb_principal': 'a_principal'}
+        default_koji_opts = {
+            'krb_rdns': False,
+            'max_retries': 30,
+            'retry_interval': 10,
+            'offline_retry': True,
+            'offline_retry_interval': 10,
+            'anon_retry': True,
+        }
+
+        client = buildsys.koji_login(config, authenticate=False)
+
+        for key in default_koji_opts:
+            self.assertEqual(default_koji_opts[key], client.opts[key])
+        self.assertEqual(type(client), koji.ClientSession)
+        # Since authenticate was False, the login should not have happened.
+        self.assertEqual(krb_login.call_count, 0)
+        # No error should have been logged
+        self.assertEqual(error.call_count, 0)
+
     # krb_login returns a bool to indicate success or failure
     @mock.patch.object(buildsys, '_koji_hub', 'http://example.com/koji')
-    @mock.patch('bodhi.server.buildsys.koji.ClientSession.krb_login',
-                mock.MagicMock(return_value=False))
+    @mock.patch('bodhi.server.buildsys.koji.ClientSession.krb_login')
     @mock.patch('bodhi.server.buildsys.log.error')
-    def test_login_failure(self, error):
+    def test_login_failure(self, error, krb_login):
         """Assert correct behavior for a failed login event."""
+        krb_login.return_value = False
         config = {'some_meaningless_other_key': 'boring_value', 'krb_ccache': 'a_ccache',
                   'krb_keytab': 'a_keytab', 'krb_principal': 'a_principal'}
 
-        client = buildsys.koji_login(config)
+        client = buildsys.koji_login(config, authenticate=True)
 
         self.assertEqual(type(client), koji.ClientSession)
+        krb_login.assert_called_once_with(ccache='a_ccache', keytab='a_keytab',
+                                          principal='a_principal')
         error.assert_called_once_with('Koji krb_login failed')
 
     # krb_login returns a bool to indicate success or failure
     @mock.patch.object(buildsys, '_koji_hub', 'http://example.com/koji')
-    @mock.patch('bodhi.server.buildsys.koji.ClientSession.krb_login',
-                mock.MagicMock(return_value=True))
+    @mock.patch('bodhi.server.buildsys.koji.ClientSession.krb_login')
     @mock.patch('bodhi.server.buildsys.log.error')
-    def test_login_success(self, error):
+    def test_login_success(self, error, krb_login):
         """Assert correct behavior for a successful login event."""
+        krb_login.return_value = True
         config = {'some_meaningless_other_key': 'boring_value', 'krb_ccache': 'a_ccache',
                   'krb_keytab': 'a_keytab', 'krb_principal': 'a_principal'}
         default_koji_opts = {
@@ -137,11 +166,13 @@ def test_login_success(self, error):
             'anon_retry': True,
         }
 
-        client = buildsys.koji_login(config)
+        client = buildsys.koji_login(config, authenticate=True)
+
         for key in default_koji_opts:
             self.assertEqual(default_koji_opts[key], client.opts[key])
-
         self.assertEqual(type(client), koji.ClientSession)
+        krb_login.assert_called_once_with(ccache='a_ccache', keytab='a_keytab',
+                                          principal='a_principal')
         # No error should have been logged
         self.assertEqual(error.call_count, 0)
 
@@ -167,6 +198,37 @@ def test_buildsys_lock(self, mock_lock, mock_buildsystem):
 class TestSetupBuildsystem(unittest.TestCase):
     """Tests :func:`bodhi.server.buildsys.setup_buildsystem` function"""
 
+    @mock.patch('bodhi.server.buildsys._buildsystem', None)
+    @mock.patch('bodhi.server.buildsys.koji.ClientSession.krb_login')
+    def test_authenticate_false(self, krb_login):
+        """If authenticate is set to False, the Koji client should should be unauthenticated."""
+        config = {
+            'krb_ccache': 'a_ccache', 'krb_keytab': 'a_keytab', 'krb_principal': 'a_principal',
+            'buildsystem': 'koji', 'koji_hub': 'https://example.com/koji'}
+        self.assertTrue(buildsys._buildsystem is None)
+
+        buildsys.setup_buildsystem(config, authenticate=False)
+
+        # Instantiating the buildsystem should not cause a krb_login to happen.
+        buildsys._buildsystem()
+        self.assertEqual(krb_login.call_count, 0)
+
+    @mock.patch('bodhi.server.buildsys._buildsystem', None)
+    @mock.patch('bodhi.server.buildsys.koji.ClientSession.krb_login')
+    def test_authenticate_true(self, krb_login):
+        """If authenticate is set to True, the Koji client should should be authenticated."""
+        config = {
+            'krb_ccache': 'a_ccache', 'krb_keytab': 'a_keytab', 'krb_principal': 'a_principal',
+            'buildsystem': 'koji', 'koji_hub': 'https://example.com/koji'}
+        self.assertTrue(buildsys._buildsystem is None)
+
+        buildsys.setup_buildsystem(config, authenticate=True)
+
+        # Instantiating the buildsystem should cause a krb_login to happen.
+        buildsys._buildsystem()
+        krb_login.assert_called_once_with(ccache='a_ccache', keytab='a_keytab',
+                                          principal='a_principal')
+
     @mock.patch('bodhi.server.buildsys._buildsystem', mock.Mock())
     def test_initialized_buildsystem(self):
         """Assert nothing happens when the buildsystem is already initialized"""
@@ -183,7 +245,7 @@ def test_koji_buildsystem(self, mock_koji_login):
         buildsys.setup_buildsystem(config)
         self.assertFalse(buildsys._buildsystem is None)
         buildsys._buildsystem()
-        mock_koji_login.assert_called_once_with(config=config)
+        mock_koji_login.assert_called_once_with(authenticate=True, config=config)
 
     @mock.patch('bodhi.server.buildsys._buildsystem', None)
     def test_dev_buildsystem(self):
diff --git a/bodhi/tests/server/test_push.py b/bodhi/tests/server/test_push.py
--- a/bodhi/tests/server/test_push.py
+++ b/bodhi/tests/server/test_push.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# Copyright © 2016-2017 Red Hat, Inc. and others.
+# Copyright © 2016-2018 Red Hat, Inc. and others.
 #
 # This file is part of Bodhi.
 #
@@ -868,3 +868,23 @@ def test_unsigned_updates_signed_updated(self, publish, mock_init):
         self.assertTrue(python_paste_deploy.date_locked <= datetime.utcnow())
         self.assertEqual(python_paste_deploy.compose.release, python_paste_deploy.release)
         self.assertEqual(python_paste_deploy.compose.request, python_paste_deploy.request)
+
+
+class TetUpdateSigStatus(base.BaseTestCase):
+    """Test the update_sig_status() function."""
+
+    @mock.patch.dict('bodhi.server.push.config',
+                     {'buildsystem': 'koji', 'koji_hub': 'https://example.com/koji'})
+    @mock.patch('bodhi.server.buildsys._buildsystem', None)
+    @mock.patch('bodhi.server.buildsys.koji.ClientSession.krb_login')
+    def test_sets_up_buildsys_without_auth(self, krb_login):
+        """
+        bodhi-push should not set up authentication for the build system.
+
+        https://github.com/fedora-infra/bodhi/issues/2190
+        """
+        u = self.db.query(models.Update).first()
+
+        push.update_sig_status(u)
+
+        self.assertEqual(krb_login.call_count, 0)