From da404f582e1ab648c7438087c025a77ab36e5733 Mon Sep 17 00:00:00 2001
From: Chenxiong Qi <cqi@redhat.com>
Date: Sep 01 2017 07:26:37 +0000
Subject: Auto acquire Kerberos credential


Two ways to acquire credential automatically. For running freshmaker in
a server, client keytab is used to acquire and store ticket in a
specified ccache. For running in local development, especially for
making a demo, kinit with personal user principal is required. In both
of these cases, ccache can be re-initialized again with new ticket when
old one expires.

Signed-off-by: Chenxiong Qi <cqi@redhat.com>

---

diff --git a/conf/config.py b/conf/config.py
index 3b53aa3..af08c01 100644
--- a/conf/config.py
+++ b/conf/config.py
@@ -1,20 +1,20 @@
 # -*- coding: utf-8 -*-
 
-from os import path
+import os
 
 
 # FIXME: workaround for this moment till confdir, dbdir (installdir etc.) are
 # declared properly somewhere/somehow
-confdir = path.abspath(path.dirname(__file__))
+confdir = os.path.abspath(os.path.dirname(__file__))
 # use parent dir as dbdir else fallback to current dir
-dbdir = path.abspath(path.join(confdir, '..')) if confdir.endswith('conf') \
+dbdir = os.path.abspath(os.path.join(confdir, '..')) if confdir.endswith('conf') \
     else confdir
 
 
 class BaseConfiguration(object):
     # Make this random (used to generate session keys)
     SECRET_KEY = '74d9e9f9cd40e66fc6c4c2e9987dce48df3ce98542529fd0'
-    SQLALCHEMY_DATABASE_URI = 'sqlite:///{0}'.format(path.join(
+    SQLALCHEMY_DATABASE_URI = 'sqlite:///{0}'.format(os.path.join(
         dbdir, 'freshmaker.db'))
     SQLALCHEMY_TRACK_MODIFICATIONS = False
 
@@ -131,6 +131,22 @@ class BaseConfiguration(object):
     # URL to ODCS to call APIs
     ODCS_SERVER_URL = ''
 
+    # Kerberos authentication Settings used to authenticated freshmaker itself
+    # by other services
+
+    # Whether to use keytab to acquire credential cache. keytab should be used
+    # in a non-devel environment.
+    KRB_AUTH_USE_KEYTAB = True
+    # Principal used to acquire credential cache. When using a client keytab,
+    # this value must be present in that keytab file. Otherwise, principal must
+    # match the one in specified ccache file.
+    KRB_AUTH_PRINCIPAL = ''
+    # Path to freshmaker's client keytab file.
+    KRB_AUTH_CLIENT_KEYTAB = ''
+    # Path to credential cache file. This optional could be None when not using
+    # a client keytab to acquire credential.
+    KRB_AUTH_CCACHE_FILE = '/tmp/freshmaker_cc_{}'.format(os.getpid())
+
 
 class DevConfiguration(BaseConfiguration):
     DEBUG = True
@@ -147,6 +163,14 @@ class DevConfiguration(BaseConfiguration):
 
     LIGHTBLUE_VERIFY_SSL = False
 
+    # During development, we usually don't need a client keytab to acquire
+    # credential. Instead, kinit in default ccache with personal principal
+    # often.
+    KRB_AUTH_USE_KEYTAB = False
+    KRB_AUTH_PRINCIPAL = ''  # Should be in form name@REAL
+    # Use the default ccache
+    KRB_AUTH_CCACHE_FILE = None
+
 
 class TestConfiguration(BaseConfiguration):
     LOG_BACKEND = 'console'
@@ -154,7 +178,7 @@ class TestConfiguration(BaseConfiguration):
     DEBUG = True
 
     SQLALCHEMY_DATABASE_URI = 'sqlite:///{0}'.format(
-        path.join(dbdir, 'tests', 'test_freshmaker.db'))
+        os.path.join(dbdir, 'tests', 'test_freshmaker.db'))
 
     MESSAGING = 'in_memory'
     PDC_URL = 'http://pdc.fedoraproject.org/rest_api/v1'
diff --git a/freshmaker/config.py b/freshmaker/config.py
index 88fe512..8096af5 100644
--- a/freshmaker/config.py
+++ b/freshmaker/config.py
@@ -236,6 +236,23 @@ class Config(object):
             'type': bool,
             'default': True,
             'desc': 'Whether to enable SSL verification over HTTP with ODCS.'},
+        'krb_auth_using_keytab': {
+            'type': bool,
+            'default': True,
+            'desc': 'Whether to acquire credential cache from a client keytab.'},
+        'krb_auth_principal': {
+            'type': str,
+            'default': True,
+            'desc': 'Principal used to acquire credential cache, which must be'
+                    ' present in specified client keytab.'},
+        'krb_auth_client_keytab': {
+            'type': str,
+            'default': '',
+            'desc': 'Path to a client keytab.'},
+        'krb_auth_ccache_file': {
+            'type': str,
+            'default': '',
+            'desc': 'Path to credential cache file.'},
     }
 
     def __init__(self, conf_section_obj):
diff --git a/freshmaker/handlers/__init__.py b/freshmaker/handlers/__init__.py
index 3cf9e60..e2b378c 100644
--- a/freshmaker/handlers/__init__.py
+++ b/freshmaker/handlers/__init__.py
@@ -27,6 +27,7 @@ import re
 from freshmaker import conf, log, db, models
 from freshmaker.mbs import MBS
 from freshmaker.kojiservice import koji_service
+from krbcontext import krbContext
 
 
 class BaseHandler(object):
@@ -54,6 +55,23 @@ class BaseHandler(object):
         """
         raise NotImplementedError()
 
+    @property
+    def krb_context(self):
+        if conf.krb_auth_use_keytab:
+            krb_ctx_opts = {
+                'using_keytab': conf.krb_auth_use_keytab,
+                'principal': conf.krb_auth_principal,
+                'keytab_file': conf.krb_auth_client_keytab,
+                'ccache_file': conf.krb_auth_ccache_file,
+            }
+        else:
+            krb_ctx_opts = {
+                'principal': conf.krb_auth_principal,
+                'ccache_file': conf.krb_auth_ccache_file,
+            }
+
+        return krbContext(**krb_ctx_opts)
+
     def build_module(self, name, branch, rev):
         """
         Build a module in MBS.
@@ -81,7 +99,9 @@ class BaseHandler(object):
         with koji_service(profile=conf.koji_profile, logger=log) as service:
             log.debug('Logging into %s with Kerberos authentication.', service.server)
             proxyuser = conf.koji_build_owner if conf.koji_proxyuser else None
-            service.krb_login(proxyuser=proxyuser)
+
+            with self.krb_context:
+                service.krb_login(proxyuser=proxyuser)
 
             if not service.logged_in:
                 log.error('Could not login server %s', service.server)
diff --git a/requirements.txt b/requirements.txt
index 216d170..b437568 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -21,3 +21,4 @@ Flask-Script
 requests
 enum34 ; python_version <= '2.7'
 odcs[client]
+krbcontext
diff --git a/tests/test_git_dockerfile_change_handler.py b/tests/test_git_dockerfile_change_handler.py
index 2ce5b33..cc755f3 100644
--- a/tests/test_git_dockerfile_change_handler.py
+++ b/tests/test_git_dockerfile_change_handler.py
@@ -65,7 +65,9 @@ class GitDockerfileChangeHandlerTest(BaseTestCase):
 
     @patch('koji.read_config')
     @patch('koji.ClientSession')
-    def test_rebuild_if_dockerfile_changed(self, ClientSession, read_config):
+    @patch('freshmaker.handlers.krbContext')
+    def test_rebuild_if_dockerfile_changed(
+            self, krbContext, ClientSession, read_config):
         read_config.return_value = {
             'server': 'https://localhost/kojihub',
             'krb_rdns': False,
@@ -77,6 +79,10 @@ class GitDockerfileChangeHandlerTest(BaseTestCase):
         msg = get_fedmsg('git_receive_dockerfile_changed')
         self.consume_fedmsg(msg)
 
+        # Kerberos context should be prepare for logging into koji by calling
+        # krb_login.
+        krbContext.assert_called_once()
+
         mock_session.krb_login.assert_called_once_with(proxyuser=None)
         mock_session.buildContainer.assert_called_once_with(
             'git://pkgs.fedoraproject.org/container/testimage.git?#e1f39d43471fc37ec82616f76a119da4eddec787',
diff --git a/tests/test_handler.py b/tests/test_handler.py
new file mode 100644
index 0000000..198759a
--- /dev/null
+++ b/tests/test_handler.py
@@ -0,0 +1,83 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2017  Red Hat, Inc.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+#
+# Written by Chenxiong Qi <cqi@redhat.com>
+
+
+from mock import patch
+from unittest import TestCase
+
+from freshmaker.handlers import BaseHandler
+
+
+class MyHandler(BaseHandler):
+    """Handler for running tests to test things defined in BaseHandler"""
+
+    def can_handle(self, event):
+        """Implement BaseHandler method"""
+
+    def handle(self, event):
+        """Implement BaseHandler method"""
+
+
+class TestKrbContextPreparedForBuildContainer(TestCase):
+    """Test krb_context for BaseHandler.build_container"""
+
+    def setUp(self):
+        self.koji_service = patch('freshmaker.handlers.koji_service')
+        self.koji_service.start()
+
+    def tearDown(self):
+        self.koji_service.stop()
+
+    @patch('freshmaker.handlers.conf')
+    @patch('freshmaker.handlers.krbContext')
+    def test_prepare_with_keytab(self, krbContext, conf):
+        conf.krb_auth_use_keytab = True
+        conf.krb_auth_principal = 'freshmaker/hostname@REALM'
+        conf.krb_auth_client_keytab = '/etc/freshmaker.keytab'
+        conf.krb_auth_ccache_file = '/tmp/freshmaker_cc'
+
+        handler = MyHandler()
+        handler.build_container('image-name', 'f26', '1234')
+
+        krbContext.assert_called_once_with(
+            using_keytab=True,
+            principal='freshmaker/hostname@REALM',
+            keytab_file='/etc/freshmaker.keytab',
+            ccache_file='/tmp/freshmaker_cc',
+        )
+
+    @patch('freshmaker.handlers.conf')
+    @patch('freshmaker.handlers.krbContext')
+    def test_prepare_with_normal_user_credential(self, krbContext, conf):
+        conf.krb_auth_use_keytab = False
+        conf.krb_auth_principal = 'somebody@REALM'
+        conf.krb_auth_ccache_file = '/tmp/freshmaker_cc'
+
+        handler = MyHandler()
+        handler.build_container('image-name', 'f26', '1234')
+
+        krbContext.assert_called_once_with(
+            principal='somebody@REALM',
+            ccache_file='/tmp/freshmaker_cc',
+        )

