Port from python-krbV to python-gssapi

python-krbV library is deprecated and doesn't work with python 3. Replacing all
it's usages with python-gssapi.

- Removed Backend.krb and KRB5_CCache classes
  They were wrappers around krbV classes that cannot really work without them
- Added few utility functions for querying GSSAPI credentials
  in krb_utils module. They provide replacements for KRB5_CCache.
- Merged two kinit_keytab functions
- Changed ldap plugin connection defaults to match ipaldap
- Unified getting default realm
  Using api.env.realm instead of krbV call

Reviewed-By: Jan Cholasta <jcholast@redhat.com>
Reviewed-By: Robbie Harwood <rharwood@redhat.com>
Reviewed-By: Simo Sorce <ssorce@redhat.com>
This commit is contained in:
Michael Simacek
2015-07-20 16:04:07 +02:00
committed by Jan Cholasta
parent aebb72e1fb
commit aad73fad60
37 changed files with 249 additions and 649 deletions

View File

@@ -16,25 +16,22 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import krbV
import time
import re
from ipapython.ipa_log_manager import *
import gssapi
from ipalib import errors
#-------------------------------------------------------------------------------
# Kerberos constants, should be defined in krbV, but aren't
KRB5_GC_CACHED = 0x2
# Kerberos error codes, should be defined in krbV, but aren't
KRB5_CC_NOTFOUND = -1765328243 # Matching credential not found
KRB5_FCC_NOFILE = -1765328189 # No credentials cache found
KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN = -1765328377 # Server not found in Kerberos database
KRB5KRB_AP_ERR_TKT_EXPIRED = -1765328352 # Ticket expired
KRB5_FCC_PERM = -1765328190 # Credentials cache permissions incorrect
KRB5_CC_FORMAT = -1765328185 # Bad format in credentials cache
KRB5_REALM_CANT_RESOLVE = -1765328164 # Cannot resolve network address for KDC in requested realm
# Kerberos error codes
KRB5_CC_NOTFOUND = 2529639053 # Matching credential not found
KRB5_FCC_NOFILE = 2529639107 # No credentials cache found
KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN = 2529638919 # Server not found in Kerberos database
KRB5KRB_AP_ERR_TKT_EXPIRED = 2529638944 # Ticket expired
KRB5_FCC_PERM = 2529639106 # Credentials cache permissions incorrect
KRB5_CC_FORMAT = 2529639111 # Bad format in credentials cache
KRB5_REALM_CANT_RESOLVE = 2529639132 # Cannot resolve network address for KDC in requested realm
krb_ticket_expiration_threshold = 60*5 # number of seconds to accmodate clock skew
krb5_time_fmt = '%m/%d/%y %H:%M:%S'
@@ -136,260 +133,66 @@ def krb5_format_time(timestamp):
'''
return time.strftime(krb5_time_fmt, time.localtime(timestamp))
class KRB5_CCache(object):
def get_credentials(name=None, ccache_name=None):
'''
Kerberos stores a TGT (Ticket Granting Ticket) and the service
tickets bound to it in a ccache (credentials cache). ccaches are
bound to a Kerberos user principal. This class opens a Kerberos
ccache and allows one to manipulate it. Most useful is the
extraction of ticket entries (cred's) in the ccache and the
ability to examine their attributes.
Obtains GSSAPI credentials with given principal name from ccache. When no
principal name specified, it retrieves the default one for given
credentials cache.
:parameters:
name
gssapi.Name object specifying principal or None for the default
ccache_name
string specifying Kerberos credentials cache name or None for the
default
:returns:
gssapi.Credentials object
'''
store = None
if ccache_name:
store = {'ccache': ccache_name}
try:
return gssapi.Credentials(usage='initiate', name=name, store=store)
except gssapi.exceptions.GSSError as e:
if e.min_code == KRB5_FCC_NOFILE:
raise ValueError('"%s", ccache="%s"' % (e.message, ccache_name))
raise
def get_principal(ccache_name=None):
'''
Gets default principal name from given credentials cache.
:parameters:
ccache_name
string specifying Kerberos credentials cache name or None for the
default
:returns:
Default principal name as string
'''
creds = get_credentials(ccache_name=ccache_name)
return unicode(creds.name)
def get_credentials_if_valid(name=None, ccache_name=None):
'''
Obtains GSSAPI credentials with principal name from ccache. When no
principal name specified, it retrieves the default one for given
credentials cache. When the credentials cannot be retrieved or aren't valid
it returns None.
:parameters:
name
gssapi.Name object specifying principal or None for the default
ccache_name
string specifying Kerberos credentials cache name or None for the
default
:returns:
gssapi.Credentials object or None if valid credentials weren't found
'''
def __init__(self, ccache):
'''
:parameters:
ccache
The name of a Kerberos ccache used to hold Kerberos tickets.
:returns:
`KRB5_CCache` object encapsulting the ccache.
'''
log_mgr.get_logger(self, True)
self.context = None
self.scheme = None
self.name = None
self.ccache = None
self.principal = None
try:
self.context = krbV.default_context()
self.scheme, self.name = krb5_parse_ccache(ccache)
self.ccache = krbV.CCache(name=str(ccache), context=self.context)
self.principal = self.ccache.principal()
except krbV.Krb5Error as e:
error_code = e.args[0]
message = e.args[1]
if error_code == KRB5_FCC_NOFILE:
raise ValueError('"%s", ccache="%s"' % (message, ccache))
else:
raise e
def ccache_str(self):
'''
A Kerberos ccache is identified by a name comprised of a
scheme and location component. This function returns that
canonical name. See `krb5_parse_ccache()`
:returns:
The name of ccache with it's scheme and location components.
'''
return '%s:%s' % (self.scheme, self.name)
def __str__(self):
return 'cache="%s" principal="%s"' % (self.ccache_str(), self.principal.name)
def get_credentials(self, principal):
'''
Given a Kerberos principal return the krbV credentials
tuple describing the credential. If the principal does
not exist in the ccache a KeyError is raised.
:parameters:
principal
The Kerberos principal whose ticket is being retrieved.
The principal may be either a string formatted as a
Kerberos V5 principal or it may be a `krbV.Principal`
object.
:returns:
A krbV credentials tuple. If the principal is not in the
ccache a KeyError is raised.
'''
if isinstance(principal, krbV.Principal):
krbV_principal = principal
else:
try:
krbV_principal = krbV.Principal(str(principal), self.context)
except Exception as e:
self.error('could not create krbV principal from "%s", %s', principal, e)
raise e
creds_tuple = (self.principal,
krbV_principal,
(0, None), # keyblock: (enctype, contents)
(0, 0, 0, 0), # times: (authtime, starttime, endtime, renew_till)
0,0, # is_skey, ticket_flags
None, # addrlist
None, # ticket_data
None, # second_ticket_data
None) # adlist
try:
cred = self.ccache.get_credentials(creds_tuple, KRB5_GC_CACHED)
except krbV.Krb5Error as e:
error_code = e.args[0]
if error_code == KRB5_CC_NOTFOUND:
raise KeyError('"%s" credential not found in "%s" ccache' % \
(krbV_principal.name, self.ccache_str()))
raise e
except Exception as e:
raise e
return cred
def get_credential_times(self, principal):
'''
Given a Kerberos principal return the ticket timestamps if the
principal's ticket in the ccache is valid. If the principal
does not exist in the ccache a KeyError is raised.
The return credential time values are Unix timestamps in
localtime.
The returned timestamps are:
authtime
The time when the ticket was issued.
starttime
The time when the ticket becomes valid.
endtime
The time when the ticket expires.
renew_till
The time when the ticket becomes no longer renewable (if renewable).
:parameters:
principal
The Kerberos principal whose ticket is being validated.
The principal may be either a string formatted as a
Kerberos V5 principal or it may be a `krbV.Principal`
object.
:returns:
return authtime, starttime, endtime, renew_till
'''
if isinstance(principal, krbV.Principal):
krbV_principal = principal
else:
try:
krbV_principal = krbV.Principal(str(principal), self.context)
except Exception as e:
self.error('could not create krbV principal from "%s", %s', principal, e)
raise e
try:
cred = self.get_credentials(krbV_principal)
authtime, starttime, endtime, renew_till = cred[3]
self.debug('get_credential_times: principal=%s, authtime=%s, starttime=%s, endtime=%s, renew_till=%s',
krbV_principal.name,
krb5_format_time(authtime), krb5_format_time(starttime),
krb5_format_time(endtime), krb5_format_time(renew_till))
return authtime, starttime, endtime, renew_till
except KeyError as e:
raise e
except Exception as e:
self.error('get_credential_times failed, principal="%s" error="%s"', krbV_principal.name, e)
raise e
def credential_is_valid(self, principal):
'''
Given a Kerberos principal return a boolean indicating if the
principal's ticket in the ccache is valid. If the ticket is
not in the ccache False is returned. If the ticket
exists in the ccache it's validity is checked and returned.
:parameters:
principal
The Kerberos principal whose ticket is being validated.
The principal may be either a string formatted as a
Kerberos V5 principal or it may be a `krbV.Principal`
object.
:returns:
True if the principal's ticket exists and is valid. False if
the ticket does not exist or if the ticket is not valid.
'''
try:
authtime, starttime, endtime, renew_till = self.get_credential_times(principal)
except KeyError as e:
return False
except Exception as e:
self.error('credential_is_valid failed, principal="%s" error="%s"', principal, e)
raise e
now = time.time()
if starttime > now:
return False
if endtime < now:
return False
return True
def valid(self, host, realm):
'''
Test to see if ldap service ticket or the TGT is valid.
:parameters:
host
ldap server
realm
kerberos realm
:returns:
True if either the ldap service ticket or the TGT is valid,
False otherwise.
'''
try:
principal = krb5_format_service_principal_name('HTTP', host, realm)
valid = self.credential_is_valid(principal)
if valid:
return True
except KeyError:
pass
try:
principal = krb5_format_tgt_principal_name(realm)
valid = self.credential_is_valid(principal)
return valid
except KeyError:
return False
def endtime(self, host, realm):
'''
Returns the minimum endtime for tickets of interest (ldap service or TGT).
:parameters:
host
ldap server
realm
kerberos realm
:returns:
UNIX timestamp value.
'''
result = 0
try:
principal = krb5_format_service_principal_name('HTTP', host, realm)
authtime, starttime, endtime, renew_till = self.get_credential_times(principal)
if result:
result = min(result, endtime)
else:
result = endtime
except KeyError:
pass
try:
principal = krb5_format_tgt_principal_name(realm)
authtime, starttime, endtime, renew_till = self.get_credential_times(principal)
if result:
result = min(result, endtime)
else:
result = endtime
except KeyError:
pass
self.debug('KRB5_CCache %s endtime=%s (%s)', self.ccache_str(), result, krb5_format_time(result))
return result
try:
creds = get_credentials(name=name, ccache_name=ccache_name)
if creds.lifetime > 0:
return creds
return None
except gssapi.exceptions.ExpiredCredentialsError:
return None

View File

@@ -1,125 +0,0 @@
# Authors:
# Jason Gerard DeRose <jderose@redhat.com>
#
# Copyright (C) 2008 Red Hat
# see file 'COPYING' for use and warranty information
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Backend plugin for Kerberos.
This wraps the python-kerberos and python-krbV bindings.
"""
import sys
from ipalib import api
from ipalib.backend import Backend
from ipalib.plugable import Registry
import krbV
register = Registry()
ENCODING = 'UTF-8'
@register()
class krb(Backend):
"""
Kerberos backend plugin.
This wraps the `krbV` bindings (and will eventually wrap the `kerberos`
bindings also). Importantly, this plugin does correct Unicode
encoding/decoding of values going-to/coming-from the bindings.
"""
def __default_ccache(self):
"""
Return the ``krbV.CCache`` for the default credential cache.
"""
return krbV.default_context().default_ccache()
def __default_principal(self):
"""
Return the ``krb5.Principal`` for the default credential cache.
"""
return self.__default_ccache().principal()
def __get_ccache(self, ccname):
"""
Return the ``krbV.CCache`` for the ``ccname`` credential ccache.
"""
return krbV.CCache(ccname)
def __get_principal(self, ccname):
"""
Return the ``krb5.Principal`` for the ``ccname`` credential ccache.
"""
return self.__get_ccache(ccname).principal()
def default_ccname(self):
"""
Return the default ccache file name (schema+name).
This will return something like 'FILE:/tmp/krb5cc_500'.
This cannot return anything meaningful if used in the server as a
request is processed.
"""
default_ccache = self.__default_ccache()
ccname = "%(type)s:%(name)s" % dict(type=default_ccache.type,
name=default_ccache.name)
return ccname
def default_principal(self):
"""
Return the principal name in default credential cache.
This will return something like 'admin@EXAMPLE.COM'. If no credential
cache exists for the invoking user, None is returned.
This cannot return anything meaningful if used in the server as a
request is processed.
"""
return self.__default_principal().name.decode(ENCODING)
def default_realm(self):
"""
Return the realm from the default credential cache.
This will return something like 'EXAMPLE.COM'. If no credential cache
exists for the invoking user, None is returned.
This cannot return anything meaningful if used in the server as a
request is processed.
"""
return krbV.default_context().default_realm.decode(ENCODING)
def get_principal(self, ccname):
"""
Return the principal from credential cache file at ``ccname``.
This will return something like 'admin@EXAMPLE.COM'.
"""
return self.__get_principal(ccname).name.decode(ENCODING)
def get_realm(self, ccname):
"""
Return the realm from credential cache file at ``ccname``.
This will return something like 'EXAMPLE.COM'.
"""
return self.__get_principal(ccname).realm.decode(ENCODING)

View File

@@ -17,7 +17,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from ipalib import api, errors, util
from ipalib import api, errors, krb_utils
from ipalib import Command
from ipalib import Str, Password
from ipalib import _
@@ -58,7 +58,7 @@ def get_current_password(principal):
current password is prompted for, otherwise return a fixed value to
be ignored later.
"""
current_principal = util.get_current_principal()
current_principal = krb_utils.get_principal()
if current_principal == normalize_principal(principal):
return None
else:
@@ -74,7 +74,7 @@ class passwd(Command):
label=_('User name'),
primary_key=True,
autofill=True,
default_from=lambda: util.get_current_principal(),
default_from=lambda: krb_utils.get_principal(),
normalizer=lambda value: normalize_principal(value),
),
Password('password',

View File

@@ -34,7 +34,6 @@ from cryptography.hazmat.primitives.serialization import load_pem_public_key,\
load_pem_private_key
import nss.nss as nss
import krbV
from ipalib.frontend import Command, Object, Local
from ipalib import api, errors
@@ -640,7 +639,7 @@ class vault_add(PKQuery, Local):
else:
backend = self.api.Backend.rpcclient
if not backend.isconnected():
backend.connect(ccache=krbV.default_context().default_ccache())
backend.connect()
if vault_type == u'standard':
@@ -1239,7 +1238,7 @@ class vault_archive(PKQuery, Local):
else:
backend = self.api.Backend.rpcclient
if not backend.isconnected():
backend.connect(ccache=krbV.default_context().default_ccache())
backend.connect()
# retrieve vault info
vault = self.api.Command.vault_show(*args, **options)['result']
@@ -1508,7 +1507,7 @@ class vault_retrieve(PKQuery, Local):
else:
backend = self.api.Backend.rpcclient
if not backend.isconnected():
backend.connect(ccache=krbV.default_context().default_ccache())
backend.connect()
# retrieve vault info
vault = self.api.Command.vault_show(*args, **options)['result']

View File

@@ -55,7 +55,6 @@ from ipalib.errors import (public_errors, UnknownError, NetworkError,
KerberosError, XMLRPCMarshallError, JSONError, ConversionError)
from ipalib import errors, capabilities
from ipalib.request import context, Connection
from ipalib.util import get_current_principal
from ipapython.ipa_log_manager import root_logger
from ipapython import ipautil
from ipapython import kernel_keyring
@@ -66,7 +65,8 @@ from ipalib.text import _
import ipapython.nsslib
from ipapython.nsslib import NSSHTTPS, NSSConnection
from ipalib.krb_utils import KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN, KRB5KRB_AP_ERR_TKT_EXPIRED, \
KRB5_FCC_PERM, KRB5_FCC_NOFILE, KRB5_CC_FORMAT, KRB5_REALM_CANT_RESOLVE
KRB5_FCC_PERM, KRB5_FCC_NOFILE, KRB5_CC_FORMAT, \
KRB5_REALM_CANT_RESOLVE, get_principal
from ipapython.dn import DN
from ipalib.capabilities import VERSION_WITHOUT_CAPABILITIES
from ipalib import api
@@ -518,10 +518,7 @@ class KerbTransport(SSLTransport):
self._sec_context = None
def _handle_exception(self, e, service=None):
# kerberos library coerced error codes to signed, gssapi uses unsigned
minor = e.min_code
if minor & (1 << 31):
minor -= 1 << 32
if minor == KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN:
raise errors.ServiceError(service=service)
elif minor == KRB5_FCC_NOFILE:
@@ -835,7 +832,7 @@ class RPCClient(Connectible):
delegate=False, nss_dir=None):
try:
rpc_uri = self.env[self.env_rpc_uri_key]
principal = get_current_principal()
principal = get_principal()
setattr(context, 'principal', principal)
# We have a session cookie, try using the session URI to see if it
# is still valid

View File

@@ -61,18 +61,6 @@ def json_serialize(obj):
return ''
return json_serialize(obj.__json__())
def get_current_principal():
try:
import gssapi
cred = gssapi.Credentials(usage='initiate')
return unicode(cred.name)
except ImportError:
raise RuntimeError('python-gssapi is not available.')
except gssapi.exceptions.GSSError:
#TODO: do a kinit?
raise errors.CCacheError()
def validate_host_dns(log, fqdn):
"""
See if the hostname has a DNS A/AAAA record.