Skip to content

Commit

Permalink
Allow initalizing kerberos credentials from keytab
Browse files Browse the repository at this point in the history
Requires klist and kinit from krb5-workstation.

Closes #127.
  • Loading branch information
mmilata committed Aug 11, 2015
1 parent 784da9d commit 734a883
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 1 deletion.
6 changes: 6 additions & 0 deletions docs/configuration_file.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ Some options are also mandatory.

* `use_kerberos` (*optional*, `boolean`) — when OpenShift is hidden behind authentication proxy, you can use kerberos for authentication

* `kerberos_keytab` (*optional*, `string`) - absolute path to a keytab that will be used to initialize kerberos credentials

* `kerberos_principal` (*optional*, `string`) - kerberos principal for the keytab provided in `kerberos_keytab`

* `kerberos_ccache` (*optional*, `string`) - absolute path to kerberos credential cache to use when `kerberos_keytab` is set (optional)

* `registry_uri` (*optional*, `string`) — docker registry URI to use for pulling and pushing images

* `pulp_registry_name` (*optional*, `string`) — name of pulp registry within dockpulp config
Expand Down
3 changes: 3 additions & 0 deletions osbs/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ def __init__(self, openshift_configuration, build_configuration):
username=self.os_conf.get_username(),
password=self.os_conf.get_password(),
use_kerberos=self.os_conf.get_use_kerberos(),
kerberos_keytab=self.os_conf.get_kerberos_keytab(),
kerberos_principal=self.os_conf.get_kerberos_principal(),
kerberos_ccache=self.os_conf.get_kerberos_ccache(),
use_auth=self.os_conf.get_use_auth(),
verify_ssl=self.os_conf.get_verify_ssl())
self._bm = None
Expand Down
6 changes: 6 additions & 0 deletions osbs/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,12 @@ def cli():
help="password within OSBS")
parser.add_argument("--use-kerberos", action='store_true', default=None,
help="use kerberos for authentication")
parser.add_argument("--kerberos-keytab", action='store',
help="path to kerberos keytab to obtain credentials from")
parser.add_argument("--kerberos-principal", action='store',
help="kerberos principal for the provided keytab")
parser.add_argument("--kerberos-ccache", action='store',
help="path to credential cache to use instead of the default one")
parser.add_argument("--verify-ssl", action='store_true', default=None,
help="verify CA on secure connections")
parser.add_argument("--with-auth", action="store_true", dest="use_auth", default=None,
Expand Down
9 changes: 9 additions & 0 deletions osbs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,15 @@ def get_password(self):
def get_use_kerberos(self):
return self._get_value("use_kerberos", self.conf_section, "use_kerberos", can_miss=True, is_bool_val=True)

def get_kerberos_keytab(self):
return self._get_value("kerberos_keytab", self.conf_section, "kerberos_keytab", can_miss=True)

def get_kerberos_principal(self):
return self._get_value("kerberos_principal", self.conf_section, "kerberos_principal", can_miss=True)

def get_kerberos_ccache(self):
return self._get_value("kerberos_ccache", self.conf_section, "kerberos_ccache", can_miss=True)

def get_registry_uri(self):
return self._get_value("registry_uri", self.conf_section, "registry_uri", can_miss=True)

Expand Down
15 changes: 14 additions & 1 deletion osbs/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import json

import logging
from osbs.kerberos_ccache import kerberos_ccache_init
from osbs.build.build_response import BuildResponse
from osbs.constants import DEFAULT_NAMESPACE, BUILD_FINISHED_STATES, BUILD_RUNNING_STATES, BUILD_CANCELLED_STATE
from osbs.constants import WATCH_MODIFIED, WATCH_DELETED, WATCH_ERROR
Expand Down Expand Up @@ -47,7 +48,8 @@ def check_response(response):
class Openshift(object):

def __init__(self, openshift_api_url, openshift_oauth_url, verbose=False,
username=None, password=None, use_kerberos=False, verify_ssl=True, use_auth=None):
username=None, password=None, use_kerberos=False, kerberos_keytab=None,
kerberos_principal=None, kerberos_ccache=None, verify_ssl=True, use_auth=None):
self.os_api_url = openshift_api_url
self._os_oauth_url = openshift_oauth_url
self.verbose = verbose
Expand All @@ -58,6 +60,9 @@ def __init__(self, openshift_api_url, openshift_oauth_url, verbose=False,
self.use_kerberos = use_kerberos
self.username = username
self.password = password
self.kerberos_keytab = kerberos_keytab
self.kerberos_principal = kerberos_principal
self.kerberos_ccache = kerberos_ccache
if use_auth is None:
self.use_auth = bool(use_kerberos or (username and password))
else:
Expand Down Expand Up @@ -106,6 +111,14 @@ def get_oauth_token(self):
username=self.username, password=self.password)
elif self.use_kerberos:
logger.info("using kerberos authentication")

if self.kerberos_keytab:
if not self.kerberos_principal:
raise OsbsAuthException("You need to provide kerberos principal along "
"with the keytab path.")
kerberos_ccache_init(self.kerberos_principal, self.kerberos_keytab,
ccache_file=self.kerberos_ccache)

r = self._get(url, with_auth=False, allow_redirects=False, kerberos_auth=True)
else:
logger.info("using identity authentication")
Expand Down
87 changes: 87 additions & 0 deletions osbs/kerberos_ccache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""
Copyright (c) 2015 Red Hat, Inc
All rights reserved.
This software may be modified and distributed under the terms
of the BSD license. See the LICENSE file for details.
"""
from __future__ import print_function, absolute_import, unicode_literals

import re
import os
import logging
import datetime
import subprocess

from osbs.exceptions import OsbsException

logger = logging.getLogger(__name__)

KLIST_TGT_RE = (r"\d\d/\d\d/\d{2,4}"
" +"
"\d\d:\d\d:\d\d"
" +"
"(?P<month>\d\d)"
"/"
"(?P<day>\d\d)"
"/"
"(?P<year>\d{2,4})"
" +"
"(?P<hour>\d\d)"
":"
"(?P<minute>\d\d)"
":"
"(?P<second>\d\d)"
" +"
"krbtgt/(?P<realm>[-.A-Z0-9]+)@(?P=realm)")

def run(cmd, extraenv={}):
env = os.environ.copy()
env.update(extraenv)

logger.debug("Subprocess: %s", ' '.join(cmd))
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env)
stdout, stderr = p.communicate()

return p.returncode, stdout, stderr

def kerberos_ccache_init(principal, keytab_file, ccache_file=None):
"""
Checks whether kerberos credential cache has ticket-granting ticket that is valid for at least
an hour.
Default ccache is used unless ccache_file is provided. In that case, KRB5CCNAME environment
variable is set to the value of ccache_file if we successfully obtain the ticket.
"""
tgt_valid = False
env = { "KRB5CCNAME": ccache_file } if ccache_file else {}

# check if we have tgt that is valid more than one hour
rc, klist, _ = run(["klist"], extraenv=env)
if rc == 0:
for line in klist.splitlines():
m = re.match(KLIST_TGT_RE, line)
if m:
# rhel6 has only last two digits
year = m.group("year")
if len(year) == 2:
year = "20" + year

expires = datetime.datetime(
int(year), int(m.group("month")), int(m.group("day")),
int(m.group("hour")), int(m.group("minute")), int(m.group("second"))
)

if expires - datetime.datetime.now() > datetime.timedelta(hours=1):
logger.debug("Valid TGT found, not renewing")
tgt_valid = True
break

if not tgt_valid:
logger.debug("Retrieving kerberos TGT")
rc, out, err = run(["kinit", "-k", "-t", keytab_file, principal], extraenv=env)
if rc != 0:
raise OsbsException("kinit returned %s:\nstdout: %s\nstderr: %s" % (rc, out, err))

if ccache_file:
os.environ["KRB5CCNAME"] = ccache_file
79 changes: 79 additions & 0 deletions tests/unit_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import os
import shutil
import sys
import datetime
from types import GeneratorType

from flexmock import flexmock
Expand All @@ -31,6 +32,7 @@
from osbs.exceptions import OsbsValidationException
from osbs import utils
from osbs.http import HttpResponse, parse_headers
import osbs.kerberos_ccache as kerberos_ccache
from tests.constants import TEST_BUILD, TEST_LABEL, TEST_LABEL_VALUE
from tests.constants import TEST_GIT_URI, TEST_GIT_REF, TEST_USER
from tests.constants import TEST_COMPONENT, TEST_TARGET, TEST_ARCH
Expand Down Expand Up @@ -804,3 +806,80 @@ def test_force_str():
s = u"s"
assert str_on_2_unicode_on_3(s) == b
assert str_on_2_unicode_on_3(b) == b

KLIST_RHEL6 = """
Ticket cache: FILE:/tmp/krb5cc_500
Default principal: user@REDBAT.COM
Valid starting Expires Service principal
08/10/15 17:36:42 %m/%d/%y %H:%M:%S krbtgt/REDBAT.COM@REDBAT.COM
08/11/15 14:13:19 08/12/15 00:13:14 imap/gmail.org@REDBAT.COM
"""

KLIST_RHEL7 = """
Ticket cache: FILE:/tmp/krb5cc_1000
Default principal: user@REDBAT.COM
Valid starting Expires Service principal
08/11/2015 08:43:56 %m/%d/%Y %H:%M:%S krbtgt/REDBAT.COM@REDBAT.COM
08/11/2015 14:13:19 08/12/15 00:13:14 imap/gmail.org@REDBAT.COM
"""

KEYTAB_PATH = '/etc/keytab'
CCACHE_PATH = '/tmp/krb5cc_thing'
PRINCIPAL = 'prin@IPAL'

@pytest.mark.parametrize("custom_ccache", [True, False])
def test_kinit_nocache(custom_ccache):
flexmock(kerberos_ccache).should_receive('run') \
.with_args(['klist']) \
.and_return(1, "", "") \
.once()
flexmock(kerberos_ccache).should_receive('run') \
.with_args(['kinit', '-k', '-t', KEYTAB_PATH, PRINCIPAL]) \
.and_return(0, "", "") \
.once()
flexmock(os.environ).should_receive('__setitem__') \
.with_args("KRB5CCNAME", CCACHE_PATH) \
.times(1 if custom_ccache else 0)

kerberos_ccache.kerberos_ccache_init(PRINCIPAL, KEYTAB_PATH, CCACHE_PATH if custom_ccache else None)

@pytest.mark.parametrize("klist_format", [KLIST_RHEL6, KLIST_RHEL7])
@pytest.mark.parametrize("custom_ccache", [True, False])
def test_kinit_recentcache(klist_format, custom_ccache):
yesterday = datetime.datetime.now() - datetime.timedelta(days=1)
klist_out = yesterday.strftime(klist_format)

flexmock(kerberos_ccache).should_receive('run') \
.with_args(['klist']) \
.and_return(0, klist_out, "") \
.once()
flexmock(kerberos_ccache).should_receive('run') \
.with_args(['kinit', '-k', '-t', KEYTAB_PATH, PRINCIPAL]) \
.and_return(0, "", "") \
.once()
flexmock(os.environ).should_receive('__setitem__') \
.with_args("KRB5CCNAME", CCACHE_PATH) \
.times(1 if custom_ccache else 0)

kerberos_ccache.kerberos_ccache_init(PRINCIPAL, KEYTAB_PATH, CCACHE_PATH if custom_ccache else None)

@pytest.mark.parametrize("klist_format", [KLIST_RHEL6, KLIST_RHEL7])
@pytest.mark.parametrize("custom_ccache", [True, False])
def test_kinit_newcache(klist_format, custom_ccache):
tomorrow = datetime.datetime.now() + datetime.timedelta(days=1)
klist_out = tomorrow.strftime(klist_format)

flexmock(kerberos_ccache).should_receive('run') \
.with_args(['klist']) \
.and_return(0, klist_out, "") \
.once()
flexmock(kerberos_ccache).should_receive('run') \
.with_args(['kinit', '-k', '-t', KEYTAB_PATH, PRINCIPAL]) \
.never()
flexmock(os.environ).should_receive('__setitem__') \
.with_args("KRB5CCNAME", CCACHE_PATH) \
.times(1 if custom_ccache else 0)

kerberos_ccache.kerberos_ccache_init(PRINCIPAL, KEYTAB_PATH, CCACHE_PATH if custom_ccache else None)

0 comments on commit 734a883

Please sign in to comment.