diff options
-rw-r--r-- | ipalib/rpc.py | 30 | ||||
-rw-r--r-- | ipapython/ccache_storage.py | 234 |
2 files changed, 242 insertions, 22 deletions
diff --git a/ipalib/rpc.py b/ipalib/rpc.py index 8d1bba5a8..be3133335 100644 --- a/ipalib/rpc.py +++ b/ipalib/rpc.py @@ -56,7 +56,7 @@ from ipalib import errors, capabilities 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 @@ if six.PY3: 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 000000000..f2de30152 --- /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) + |