From da404f582e1ab648c7438087c025a77ab36e5733 Mon Sep 17 00:00:00 2001 From: Chenxiong Qi 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 --- 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 + + +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', + )