URL: https://github.com/freeipa/freeipa/pull/543 Author: simo5 Title: #543: Add options to allow ticket caching Action: synchronized
To pull the PR as Git branch: git remote add ghfreeipa https://github.com/freeipa/freeipa git fetch ghfreeipa pull/543/head:pr543 git checkout pr543
From 513c118d741594bf6bab6302a4b24c23168c4c44 Mon Sep 17 00:00:00 2001 From: Simo Sorce <s...@redhat.com> Date: Mon, 6 Mar 2017 13:46:44 -0500 Subject: [PATCH 1/3] Add options to allow ticket caching This new option (planned to land in gssproxy 0.7) we cache the ldap ticket properly and avoid a ticket lookup to the KDC on each and every ldap connection. (Also requires krb5 libs 1.15.1 to benefit from caching). Signed-off-by: Simo Sorce <s...@redhat.com> --- install/share/gssproxy.conf.template | 2 ++ 1 file changed, 2 insertions(+) diff --git a/install/share/gssproxy.conf.template b/install/share/gssproxy.conf.template index fbb158a..9d11100 100644 --- a/install/share/gssproxy.conf.template +++ b/install/share/gssproxy.conf.template @@ -4,6 +4,7 @@ cred_store = keytab:$HTTP_KEYTAB cred_store = client_keytab:$HTTP_KEYTAB allow_protocol_transition = true + allow_client_ccache_sync = true cred_usage = both euid = $HTTPD_USER @@ -12,5 +13,6 @@ cred_store = keytab:$HTTP_KEYTAB cred_store = client_keytab:$HTTP_KEYTAB allow_constrained_delegation = true + allow_client_ccache_sync = true cred_usage = initiate euid = $IPAAPI_USER From 34553627ebd709dea371030b03607c9c167732b0 Mon Sep 17 00:00:00 2001 From: Simo Sorce <s...@redhat.com> Date: Mon, 6 Mar 2017 14:19:30 -0500 Subject: [PATCH 2/3] Use GSS-SPNEGO if connecting locally GSS-SPNEGO allows us to negotiate a sasl bind with less roundrtrips therefore use it when possible. We only enable it for local connections for now because we only recently fixed Cyrus SASL to do proper GSS-SPNEGO negotiation. This change means a newer and an older version are not compatible. Restricting ourselves to the local host prevents issues with incomaptible services, and it is ok for us as we are only really lloking at speedups for the local shortlived connections performed by the framework. Most other clients have llonger lived connections, so peformance improvements there are not as important. Signed-off-by: Simo Sorce <s...@redhat.com> --- ipapython/ipaldap.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ipapython/ipaldap.py b/ipapython/ipaldap.py index 82d45b9..b158598 100644 --- a/ipapython/ipaldap.py +++ b/ipapython/ipaldap.py @@ -52,6 +52,7 @@ # Global variable to define SASL auth SASL_GSSAPI = ldap.sasl.sasl({}, 'GSSAPI') +SASL_GSS_SPNEGO = ldap.sasl.sasl({}, 'GSS-SPNEGO') _debug_log_ldap = False @@ -1112,7 +1113,10 @@ def gssapi_bind(self, server_controls=None, client_controls=None): Perform SASL bind operation using the SASL GSSAPI mechanism. """ with self.error_handler(): - auth_tokens = ldap.sasl.sasl({}, 'GSSAPI') + if self._protocol == 'ldapi': + auth_tokens = SASL_GSS_SPNEGO + else: + auth_tokens = SASL_GSSAPI self._flush_schema() self.conn.sasl_interactive_bind_s( '', auth_tokens, server_controls, client_controls) From 4a9b4a7769e36890f95d87053388579928088dd3 Mon Sep 17 00:00:00 2001 From: Simo Sorce <s...@redhat.com> Date: Mon, 6 Mar 2017 18:47:56 -0500 Subject: [PATCH 3/3] Store session cookie in a ccache option Instead of using the kernel keyring,s tore the session cookie within the ccache. This way kdestroy will really wipe away all creedntials. Ticket: https://pagure.io/freeipa/issue/6661 Signed-off-by: Simo Sorce <s...@redhat.com> --- ipalib/rpc.py | 30 ++---- ipapython/ccache_storage.py | 234 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 242 insertions(+), 22 deletions(-) create mode 100644 ipapython/ccache_storage.py diff --git a/ipalib/rpc.py b/ipalib/rpc.py index 8d1bba5..be31333 100644 --- a/ipalib/rpc.py +++ b/ipalib/rpc.py @@ -56,7 +56,7 @@ from ipalib.request import context, Connection from ipapython.ipa_log_manager import root_logger from ipapython import ipautil -from ipapython import kernel_keyring +from ipapython import ccache_storage from ipapython.cookie import Cookie from ipapython.dnsutil import DNSName from ipalib.text import _ @@ -84,19 +84,11 @@ unicode = str COOKIE_NAME = 'ipa_session' -KEYRING_COOKIE_NAME = '%s_cookie:%%s' % COOKIE_NAME +CCACHE_COOKIE_KEY_NAME = 'X-IPA-Session-Cookie' errors_by_code = dict((e.errno, e) for e in public_errors) -def client_session_keyring_keyname(principal): - ''' - Return the key name used for storing the client session data for - the given principal. - ''' - - return KEYRING_COOKIE_NAME % principal - def update_persistent_client_session_data(principal, data): ''' Given a principal create or update the session data for that @@ -106,13 +98,11 @@ def update_persistent_client_session_data(principal, data): ''' try: - keyname = client_session_keyring_keyname(principal) + s = ccache_storage.session_store(CCACHE_COOKIE_KEY_NAME) + s.store_data(principal, data) except Exception as e: raise ValueError(str(e)) - # kernel_keyring only raises ValueError (why??) - kernel_keyring.update_key(keyname, data) - def read_persistent_client_session_data(principal): ''' Given a principal return the stored session data for that @@ -122,13 +112,11 @@ def read_persistent_client_session_data(principal): ''' try: - keyname = client_session_keyring_keyname(principal) + s = ccache_storage.session_store(CCACHE_COOKIE_KEY_NAME) + s.get_data(principal) except Exception as e: raise ValueError(str(e)) - # kernel_keyring only raises ValueError (why??) - return kernel_keyring.read_key(keyname) - def delete_persistent_client_session_data(principal): ''' Given a principal remove the session data for that @@ -138,13 +126,11 @@ def delete_persistent_client_session_data(principal): ''' try: - keyname = client_session_keyring_keyname(principal) + s = ccache_storage.session_store(CCACHE_COOKIE_KEY_NAME) + s.remove_data(principal) except Exception as e: raise ValueError(str(e)) - # kernel_keyring only raises ValueError (why??) - kernel_keyring.del_key(keyname) - def xml_wrap(value, version): """ Wrap all ``str`` in ``xmlrpc.client.Binary``. diff --git a/ipapython/ccache_storage.py b/ipapython/ccache_storage.py new file mode 100644 index 0000000..f2de301 --- /dev/null +++ b/ipapython/ccache_storage.py @@ -0,0 +1,234 @@ +# +# Copyright (C) 2017 FreeIPA Contributors see COPYING for license +# + +import ctypes +import os +import sys + +import six + + +class KRB5Error(Exception): + pass + + +PY3 = sys.version_info[0] == 3 + + +try: + LIBKRB5 = ctypes.CDLL('libkrb5.so.3') +except OSError as e: # pragma: no cover + LIBKRB5 = e +else: + class c_text_p(ctypes.c_char_p): # noqa + """A c_char_p variant that can handle UTF-8 text""" + @classmethod + def from_param(cls, value): + if value is None: + return None + if PY3 and isinstance(value, str): + return value.encode('utf-8') + elif not PY3 and isinstance(value, unicode): # noqa + return value.encode('utf-8') + elif not isinstance(value, bytes): + raise TypeError(value) + else: + return value + + @property + def text(self): + value = self.value + if value is None: + return None + elif not isinstance(value, str): + return value.decode('utf-8') + return value + + class _krb5_context(ctypes.Structure): # noqa + """krb5/krb5.h struct _krb5_context""" + __slots__ = () + _fields_ = [] + + class _krb5_ccache(ctypes.Structure): # noqa + """krb5/krb5.h struct _krb5_ccache""" + __slots__ = () + _fields_ = [] + + class _krb5_data(ctypes.Structure): # noqa + """krb5/krb5.h struct _krb5_data""" + __slots__ = () + _fields_ = [ + ("magic", ctypes.c_int32), + ("length", ctypes.c_uint), + ("data", ctypes.c_char_p), + ] + + class krb5_principal_data(ctypes.Structure): # noqa + """krb5/krb5.h struct krb5_principal_data""" + __slots__ = () + _fields_ = [] + + def krb5_errcheck(result, func, arguments): + """Error checker for krb5_error return value""" + if result != 0: + raise KRB5Error(result, func.__name__, arguments) + + krb5_principal = ctypes.POINTER(krb5_principal_data) + krb5_context = ctypes.POINTER(_krb5_context) + krb5_ccache = ctypes.POINTER(_krb5_ccache) + krb5_data_p = ctypes.POINTER(_krb5_data) + krb5_error = ctypes.c_int32 + + krb5_init_context = LIBKRB5.krb5_init_context + krb5_init_context.argtypes = (ctypes.POINTER(krb5_context), ) + krb5_init_context.restype = krb5_error + krb5_init_context.errcheck = krb5_errcheck + + krb5_free_context = LIBKRB5.krb5_free_context + krb5_free_context.argtypes = (krb5_context, ) + krb5_free_context.retval = None + + krb5_free_principal = LIBKRB5.krb5_free_principal + krb5_free_principal.argtypes = (krb5_context, krb5_principal) + krb5_free_principal.retval = None + + krb5_free_data_contents = LIBKRB5.krb5_free_data_contents + krb5_free_data_contents.argtypes = (krb5_context, krb5_data_p) + krb5_free_data_contents.retval = None + + krb5_cc_default = LIBKRB5.krb5_cc_default + krb5_cc_default.argtypes = (krb5_context, ctypes.POINTER(krb5_ccache), ) + krb5_cc_default.restype = krb5_error + krb5_cc_default.errcheck = krb5_errcheck + + krb5_cc_close = LIBKRB5.krb5_cc_close + krb5_cc_close.argtypes = (krb5_context, krb5_ccache, ) + krb5_cc_close.retval = krb5_error + krb5_cc_close.errcheck = krb5_errcheck + + krb5_parse_name = LIBKRB5.krb5_parse_name + krb5_parse_name.argtypes = (krb5_context, ctypes.c_char_p, + ctypes.POINTER(krb5_principal), ) + krb5_parse_name.retval = krb5_error + krb5_parse_name.errcheck = krb5_errcheck + + krb5_cc_set_config = LIBKRB5.krb5_cc_set_config + krb5_cc_set_config.argtypes = (krb5_context, krb5_ccache, krb5_principal, + ctypes.c_char_p, krb5_data_p, ) + krb5_cc_set_config.retval = krb5_error + krb5_cc_set_config.errcheck = krb5_errcheck + + krb5_cc_get_config = LIBKRB5.krb5_cc_get_config + krb5_cc_get_config.argtypes = (krb5_context, krb5_ccache, krb5_principal, + ctypes.c_char_p, krb5_data_p, ) + krb5_cc_get_config.retval = krb5_error + krb5_cc_get_config.errcheck = krb5_errcheck + +class session_store: + def __init__(self, name='X-IPA-Session-Cookie'): + self.__context = None + if isinstance(LIBKRB5, Exception): # pragma: no cover + raise LIBKRB5 + context = krb5_context() + krb5_init_context(ctypes.byref(context)) + self.__context = context + + self._hidden_cred_name = name + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + if self.__context: + krb5_free_context(self.__context) + self.__context = None + + def __del__(self): + self.__exit__(None, None, None) + + def store_cookie(self, client, value): + """ + Stores the session cookie in a hidden ccache entry. + """ + assert isinstance(client, six.string_types) + assert isinstance(value, bytes) + + principal = ccache = None + + try: + principal = krb5_principal() + krb5_parse_name(self.__context, ctypes.c_char_p(client), + ctypes.byref(principal)) + + ccache = krb5_ccache() + krb5_cc_default(self.__context, ctypes.byref(ccache)) + + buf = ctypes.create_string_buffer(value) + data = _krb5_data() + data.data = buf.value + data.length = len(buf) + krb5_cc_set_config(self.__context, ccache, principal, + self._hidden_cred_name, ctypes.byref(data)) + + finally: + if principal: + krb5_free_principal(self.__context, principal) + if ccache: + krb5_cc_close(self.__context, ccache) + + def get_cookie(self, client): + """ + Gets the session cookie in a hidden ccache entry. + """ + assert isinstance(client, six.string_types) + + principal = ccache = data = None + + try: + principal = krb5_principal() + krb5_parse_name(self.__context, ctypes.c_char_p(client), + ctypes.byref(principal)) + + ccache = krb5_ccache() + krb5_cc_default(self.__context, ctypes.byref(ccache)) + + data = _krb5_data() + krb5_cc_get_config(self.__context, ccache, principal, + self._hidden_cred_name, ctypes.byref(data)) + + return str(data.data) + + finally: + if principal: + krb5_free_principal(self.__context, principal) + if ccache: + krb5_cc_close(self.__context, ccache) + if data: + krb5_free_data_contents(self.__context, data) + + def remove_cookie(self, client): + """ + Stores the session cookie in a hidden ccache entry. + """ + assert isinstance(client, six.string_types) + + principal = ccache = None + + try: + principal = krb5_principal() + krb5_parse_name(self.__context, ctypes.c_char_p(client), + ctypes.byref(principal)) + + ccache = krb5_ccache() + krb5_cc_default(self.__context, ctypes.byref(ccache)) + + krb5_cc_set_config(self.__context, ccache, principal, + self._hidden_cred_name, None) + + finally: + if principal: + krb5_free_principal(self.__context, principal) + if ccache: + krb5_cc_close(self.__context, ccache) +
-- Manage your subscription for the Freeipa-devel mailing list: https://www.redhat.com/mailman/listinfo/freeipa-devel Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code