Petr Viktorin wrote:
On 06/11/2012 06:49 PM, Martin Kosek wrote:
On Thu, 2012-06-07 at 22:55 -0400, Rob Crittenden wrote:
Rob Crittenden wrote:
Rob Crittenden wrote:
This adds client session support. The session key is stored in the
kernel key ring.

Your first request should go to /ipa/session/xml where it should be
rejected with a 401. The next will go to /ipa/xml which will be
accepted. This should all be invisible to the client.

Subsequent requests should go to /ipa/session/xml which should let you
in with the cookie.

You can add the -vv option after ipa to see fully what is going on,
e.g.
ipa -vv user-show admin

To manage your keyring use the keyctl command like:

$ keyctl list @s
2 keys in keyring:
353548226: --alswrv 1000 -1 keyring: _uid.1000
941350591: --alswrv 1000 1000 user: ipa_session_cookie

To remove a key:

$ keyctl unlink 941350591 @s

rob

Hmm, this doesn't play too nice with the lite-server. Let me see if I
can track it down. The ccache is being removed, probably as part of the
session code. Sessions don't make sense with the lite server since it
uses the local ccache directly.

Updated patch. Don't clean up the ccache if in the lite-server.

rob


Good job there. I tested various scenarios (2 master, fallback with SRV
records, old client (RHEL 6.2)) and most worked for me, but only I
worked under the root account. This is what I got with non-root:

$ ipa user-show admin
...
ipa: DEBUG: stderr=
ipa: DEBUG: args=keyctl search @s user ipa_session_cookie
ipa: DEBUG: stdout=113632397

ipa: DEBUG: stderr=
ipa: DEBUG: args=keyctl pupdate 113632397
ipa: DEBUG: stdout=
ipa: DEBUG: stderr=keyctl_update: Permission denied
ipa: INFO: trying https://vm-131.idm.lab.bos.redhat.com/ipa/session/xml
ipa: DEBUG: NSSConnection init vm-131.idm.lab.bos.redhat.com
ipa: ERROR: cannot connect to 'any of the configured servers': ...

Shouldn't we use @us instead of @s for storing user session keys?


Secondly, I wonder if we also plan to add some logout command? This way
even if I do kdestroy, the session still exist and someone other may
still execute commands.

Martin

Also: keyctl is in the keyutils package, which we need to depend on.


Nice catch, updated patch.

I also included a bit more about why I chose @s instead of @us. Basically it is so a different shell can have a different session and therefore a different identity.

I'm going to open a ticket for the logout. For the short-term one can do something like:

$ keyctl purge user

Or more precisely:

$ keyctl list @s
2 keys in keyring:
353548226: --alswrv  1000    -1 keyring: _uid.1000
207626975: --alswrv  1000  1000 user: ipa_session_cookie
$ keyctl unlink 207626975
1 links removed

rob


>From 9c28626140ceeb0c4b9ce2879a1a18273d9696da Mon Sep 17 00:00:00 2001
From: Rob Crittenden <rcrit...@redhat.com>
Date: Wed, 6 Jun 2012 22:54:16 -0400
Subject: [PATCH] Store session cookie in ccache for cli users

Try to use the URI /ipa/session/xml if there is a key in the kernel
keyring. If there is no cookie or it turns out to be invalid (expired,
whatever) then use the standard URI /ipa/xml. This in turn will create
a session that the user can then use later.

https://fedorahosted.org/freeipa/ticket/2331
---
 freeipa.spec.in                      |    1 +
 install/conf/ipa.conf                |   10 +-
 ipalib/rpc.py                        |   84 +++++++++++--
 ipapython/kernel_keyring.py          |  102 +++++++++++++++
 ipaserver/plugins/xmlserver.py       |    3 +-
 ipaserver/rpcserver.py               |  229 ++++++++++++++++++++++++----------
 tests/test_ipapython/test_keyring.py |  147 ++++++++++++++++++++++
 7 files changed, 497 insertions(+), 79 deletions(-)
 create mode 100644 ipapython/kernel_keyring.py
 create mode 100644 tests/test_ipapython/test_keyring.py

diff --git a/freeipa.spec.in b/freeipa.spec.in
index 17c748b6dc9e3b1b491bc6e0b49f915834adf951..e608979631c46dc3c7f3c10b0be5a6246ca8afa0 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -154,6 +154,7 @@ Requires(preun):  python initscripts chkconfig
 Requires(postun): python initscripts chkconfig
 %endif
 Requires: python-dns
+Requires: keyutils
 
 # We have a soft-requires on bind. It is an optional part of
 # IPA but if it is configured we need a way to require versions
diff --git a/install/conf/ipa.conf b/install/conf/ipa.conf
index 89c9849ca6656ae3da585a72392d5d2463f4d892..948fee6fc10d35c3fefb82d0f3ed09cefb9b16e8 100644
--- a/install/conf/ipa.conf
+++ b/install/conf/ipa.conf
@@ -1,5 +1,7 @@
 #
-# VERSION 4 - DO NOT REMOVE THIS LINE
+# VERSION 5 - DO NOT REMOVE THIS LINE
+#
+# This file may be overwritten on upgrades.
 #
 # LoadModule auth_kerb_module modules/mod_auth_kerb.so
 
@@ -66,6 +68,12 @@ KrbConstrainedDelegationLock ipa
   Allow from all
 </Location>
 
+<Location "/ipa/session/xml">
+  Satisfy Any
+  Order Deny,Allow
+  Allow from all
+</Location>
+
 <Location "/ipa/session/login_password">
   Satisfy Any
   Order Deny,Allow
diff --git a/ipalib/rpc.py b/ipalib/rpc.py
index bd18b6bbf566362e9954bd25235512dda7baaffc..b8f5987377b6eea74268a2c3f07bf6f1ecefdd54 100644
--- a/ipalib/rpc.py
+++ b/ipalib/rpc.py
@@ -47,6 +47,7 @@ from ipalib.errors import public_errors, PublicError, UnknownError, NetworkError
 from ipalib import errors
 from ipalib.request import context, Connection
 from ipapython import ipautil
+from ipapython import kernel_keyring
 
 import httplib
 import socket
@@ -257,6 +258,13 @@ class SSLTransport(LanguageAwareTransport):
         conn.connect()
         return conn
 
+    def parse_response(self, response):
+        session_cookie = response.getheader('Set-Cookie')
+        if session_cookie:
+            kernel_keyring.update_key('ipa_session_cookie', session_cookie)
+        return LanguageAwareTransport.parse_response(self, response)
+
+
 class KerbTransport(SSLTransport):
     """
     Handles Kerberos Negotiation authentication to an XML-RPC server.
@@ -281,8 +289,20 @@ class KerbTransport(SSLTransport):
             raise errors.KerberosError(major=major, minor=minor)
 
     def get_host_info(self, host):
+        """
+        Two things can happen here. If we have a session we will add
+        a cookie for that. If not we will set an Authorization header.
+        """
         (host, extra_headers, x509) = SSLTransport.get_host_info(self, host)
 
+        if not isinstance(extra_headers, list):
+            extra_headers = []
+
+        session_data = getattr(context, 'session_data', None)
+        if session_data:
+            extra_headers.append(('Cookie', session_data))
+            return (host, extra_headers, x509)
+
         # Set the remote host principal
         service = "HTTP@" + host.split(':')[0]
 
@@ -296,9 +316,6 @@ class KerbTransport(SSLTransport):
         except kerberos.GSSError, e:
             self._handle_exception(e, service=service)
 
-        if not isinstance(extra_headers, list):
-            extra_headers = []
-
         for (h, v) in extra_headers:
             if h == 'Authorization':
                 extra_headers.remove((h, v))
@@ -345,12 +362,12 @@ class xmlclient(Connectible):
         server = '%s://%s%s' % (scheme, ipautil.format_netloc(self.conn._ServerProxy__host), self.conn._ServerProxy__handler)
         return server
 
-    def get_url_list(self):
+    def get_url_list(self, xmlrpc_uri):
         """
         Create a list of urls consisting of the available IPA servers.
         """
         # the configured URL defines what we use for the discovered servers
-        (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(self.env.xmlrpc_uri)
+        (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(xmlrpc_uri)
         servers = []
         name = '_ldap._tcp.%s.' % self.env.domain
 
@@ -366,7 +383,7 @@ class xmlclient(Connectible):
         servers = list(set(servers))
         # the list/set conversion won't preserve order so stick in the
         # local config file version here.
-        cfg_server = self.env.xmlrpc_uri
+        cfg_server = xmlrpc_uri
         if cfg_server in servers:
             # make sure the configured master server is there just once and
             # it is the first one
@@ -379,7 +396,21 @@ class xmlclient(Connectible):
 
     def create_connection(self, ccache=None, verbose=False, fallback=True,
                           delegate=False):
-        servers = self.get_url_list()
+        try:
+            session = False
+            xmlrpc_uri = self.env.xmlrpc_uri
+            # We have a session cookie, try using the session URI to see if it
+            # is still valid
+            if not delegate:
+                session_data = kernel_keyring.read_key('ipa_session_cookie')
+                setattr(context, 'session_data', session_data)
+                (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(self.env.xmlrpc_uri)
+                xmlrpc_uri = urlparse.urlunparse((scheme, netloc, '/ipa/session/xml', params, query, fragment))
+                session = True
+        except ValueError:
+            # No session key, do full Kerberos auth
+            pass
+        servers = self.get_url_list(xmlrpc_uri)
         serverproxy = None
         for server in servers:
             kw = dict(allow_none=True, encoding='UTF-8')
@@ -393,9 +424,10 @@ class xmlclient(Connectible):
                 kw['transport'] = LanguageAwareTransport()
             self.log.info('trying %s' % server)
             serverproxy = ServerProxy(server, **kw)
-            if len(servers) == 1 or not fallback:
-                # if we have only 1 server to try then let the main
-                # requester handle any errors
+            if len(servers) == 1:
+                # if we have only 1 server and then let the
+                # main requester handle any errors. This also means it
+                # must handle a 401 but we save a ping.
                 return serverproxy
             try:
                 command = getattr(serverproxy, 'ping')
@@ -417,9 +449,23 @@ class xmlclient(Connectible):
             except KerberosError, krberr:
                 # kerberos error on one server is likely on all
                 raise errors.KerberosError(major=str(krberr), minor='')
+            except ProtocolError, e:
+                if session_data and e.errcode == 401:
+                    # Unauthorized. Remove the session and try again.
+                    try:
+                        kernel_keyring.del_key('ipa_session_cookie')
+                        delattr(context, 'session_data')
+                    except ValueError:
+                        # This shouldn't happen if we have a session but
+                        # it isn't fatal.
+                        pass
+                    return self.create_connection(ccache, verbose, fallback, delegate)
+                if not fallback:
+                    raise
+                serverproxy = None
             except Exception, e:
                 if not fallback:
-                    raise e
+                    raise
                 serverproxy = None
 
         if serverproxy is None:
@@ -466,6 +512,22 @@ class xmlclient(Connectible):
         except NSPRError, e:
             raise NetworkError(uri=server, error=str(e))
         except ProtocolError, e:
+            # By catching a 401 here we can detect the case where we have
+            # a single IPA server and the session is invalid. Otherwise
+            # we always have to do a ping().
+            session_data = getattr(context, 'session_data', None)
+            if session_data and e.errcode == 401:
+                # Unauthorized. Remove the session and try again.
+                try:
+                    kernel_keyring.del_key('ipa_session_cookie')
+                    delattr(context, 'session_data')
+                except ValueError:
+                    # This shouldn't happen if we have a session but
+                    # it isn't fatal.
+                    pass
+                serverproxy = self.create_connection(os.environ.get('KRB5CCNAME'), self.env.verbose, self.env.fallback, self.env.delegate)
+                setattr(context, self.id, Connection(serverproxy, self.disconnect))
+                return self.forward(name, *args, **kw)
             raise NetworkError(uri=server, error=e.errmsg)
         except socket.error, e:
             raise NetworkError(uri=server, error=str(e))
diff --git a/ipapython/kernel_keyring.py b/ipapython/kernel_keyring.py
new file mode 100644
index 0000000000000000000000000000000000000000..547dd3de6b45295910b66982e99886135c06335b
--- /dev/null
+++ b/ipapython/kernel_keyring.py
@@ -0,0 +1,102 @@
+# Authors: Rob Crittenden <rcrit...@redhat.com>
+#
+# Copyright (C) 2012  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/>.
+#
+
+from ipapython.ipautil import run
+
+# NOTE: Absolute path not required for keyctl since we reset the environment
+#       in ipautil.run.
+
+# Use the session keyring so the same user can have a different principal
+# in different shells. This was explicitly chosen over @us because then
+# it is not possible to use KRB5CCNAME to have a different user principal.
+# The same session would always be used and the first principal would
+# always win.
+KEYRING = '@s'
+KEYTYPE = 'user'
+
+def dump_keys():
+    """
+    Dump all keys
+    """
+    (stdout, stderr, rc) = run(['keyctl', 'list', KEYRING], raiseonerr=False)
+    return stdout
+
+def get_real_key(key):
+    """
+    One cannot request a key based on the description it was created with
+    so find the one we're looking for.
+    """
+    (stdout, stderr, rc) = run(['keyctl', 'search', KEYRING, KEYTYPE, key], raiseonerr=False)
+    if rc:
+        raise ValueError('key %s not found' % key)
+    return stdout.rstrip()
+
+def has_key(key):
+    """
+    Returns True/False whether the key exists in the keyring.
+    """
+    try:
+        get_real_key(key)
+        return True
+    except ValueError:
+        return False
+
+def read_key(key):
+    """
+    Read the keyring and return the value for key.
+
+    Use pipe instead of print here to ensure we always get the raw data.
+    """
+    real_key = get_real_key(key)
+    (stdout, stderr, rc) = run(['keyctl', 'pipe', real_key], raiseonerr=False)
+    if rc:
+        raise ValueError('keyctl pipe failed: %s' % stderr)
+
+    return stdout
+
+def update_key(key, value):
+    """
+    Update the keyring data. If they key doesn't exist it is created.
+    """
+    if has_key(key):
+        real_key = get_real_key(key)
+        (stdout, stderr, rc) = run(['keyctl', 'pupdate', real_key], stdin=value, raiseonerr=False)
+        if rc:
+            raise ValueError('keyctl pupdate failed: %s' % stderr)
+    else:
+        add_key(key, value)
+
+def add_key(key, value):
+    """
+    Add a key to the kernel keyring.
+    """
+    if has_key(key):
+        raise ValueError('key %s already exists' % key)
+    (stdout, stderr, rc) = run(['keyctl', 'padd', KEYTYPE, key, KEYRING], stdin=value, raiseonerr=False)
+    if rc:
+        raise ValueError('keyctl padd failed: %s' % stderr)
+
+def del_key(key):
+    """
+    Remove a key from the keyring
+    """
+    real_key = get_real_key(key)
+    (stdout, stderr, rc) = run(['keyctl', 'unlink', real_key, KEYRING], raiseonerr=False)
+    if rc:
+        raise ValueError('keyctl unlink failed: %s' % stderr)
diff --git a/ipaserver/plugins/xmlserver.py b/ipaserver/plugins/xmlserver.py
index 4ae914950b3dc4f593f03853dc1450d1dfea78d5..07bf6d1e5c61224bd71e61d9e23e0ff824cd5046 100644
--- a/ipaserver/plugins/xmlserver.py
+++ b/ipaserver/plugins/xmlserver.py
@@ -25,10 +25,11 @@ Loads WSGI server plugins.
 from ipalib import api
 
 if 'in_server' in api.env and api.env.in_server is True:
-    from ipaserver.rpcserver import wsgi_dispatch, xmlserver, jsonserver_kerb, jsonserver_session, login_kerberos, login_password
+    from ipaserver.rpcserver import wsgi_dispatch, xmlserver, jsonserver_kerb, jsonserver_session, login_kerberos, login_password, xmlserver_session
     api.register(wsgi_dispatch)
     api.register(xmlserver)
     api.register(jsonserver_kerb)
     api.register(jsonserver_session)
     api.register(login_kerberos)
     api.register(login_password)
+    api.register(xmlserver_session)
diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py
index f9a549f4e8e0135e68c3a2ae8a9e876cac0d88df..30a1d5727625b8dd83d71beed07f8368876573b0 100644
--- a/ipaserver/rpcserver.py
+++ b/ipaserver/rpcserver.py
@@ -383,72 +383,6 @@ class WSGIExecutioner(Executioner):
         raise NotImplementedError('%s.marshal()' % self.fullname)
 
 
-class xmlserver(WSGIExecutioner, HTTP_Status):
-    """
-    Execution backend plugin for XML-RPC server.
-
-    Also see the `ipalib.rpc.xmlclient` plugin.
-    """
-
-    content_type = 'text/xml'
-    key = '/xml'
-
-    def _on_finalize(self):
-        self.__system = {
-            'system.listMethods': self.listMethods,
-            'system.methodSignature': self.methodSignature,
-            'system.methodHelp': self.methodHelp,
-        }
-        super(xmlserver, self)._on_finalize()
-
-    def __call__(self, environ, start_response):
-        '''
-        '''
-
-        self.debug('WSGI xmlserver.__call__:')
-        user_ccache=environ.get('KRB5CCNAME')
-        if user_ccache is None:
-            self.internal_error(environ, start_response,
-                                'xmlserver.__call__: KRB5CCNAME not defined in HTTP request environment')
-            return self.marshal(None, CCacheError())
-        try:
-            self.create_context(ccache=user_ccache)
-            response = super(xmlserver, self).__call__(environ, start_response)
-        except PublicError, e:
-            status = HTTP_STATUS_SUCCESS
-            response = status
-            headers = [('Content-Type', 'text/plain; charset=utf-8')]
-            start_response(status, headers)
-            return self.marshal(None, e)
-        finally:
-            destroy_context()
-        return response
-
-    def listMethods(self, *params):
-        return tuple(name.decode('UTF-8') for name in self.Command)
-
-    def methodSignature(self, *params):
-        return u'methodSignature not implemented'
-
-    def methodHelp(self, *params):
-        return u'methodHelp not implemented'
-
-    def unmarshal(self, data):
-        (params, name) = xml_loads(data)
-        (args, options) = params_2_args_options(params)
-        return (name, args, options, None)
-
-    def marshal(self, result, error, _id=None):
-        if error:
-            self.debug('response: %s: %s', error.__class__.__name__, str(error))
-            response = Fault(error.errno, error.strerror)
-        else:
-            if isinstance(result, dict):
-                self.debug('response: entries returned %d', result.get('count', 1))
-            response = (result,)
-        return xml_dumps(response, methodresponse=True)
-
-
 def json_encode_binary(val):
     '''
    JSON cannot encode binary values. We encode binary values in Python str
@@ -745,6 +679,76 @@ class KerberosSession(object):
         return ['']
 
 
+class xmlserver(WSGIExecutioner, HTTP_Status, KerberosSession):
+    """
+    Execution backend plugin for XML-RPC server.
+
+    Also see the `ipalib.rpc.xmlclient` plugin.
+    """
+
+    content_type = 'text/xml'
+    key = '/xml'
+
+    def _on_finalize(self):
+        self.__system = {
+            'system.listMethods': self.listMethods,
+            'system.methodSignature': self.methodSignature,
+            'system.methodHelp': self.methodHelp,
+        }
+        super(xmlserver, self)._on_finalize()
+        self.kerb_session_on_finalize()
+
+    def __call__(self, environ, start_response):
+        '''
+        '''
+
+        self.debug('WSGI xmlserver.__call__:')
+        user_ccache=environ.get('KRB5CCNAME')
+        if user_ccache is None:
+            self.internal_error(environ, start_response,
+                                'xmlserver.__call__: KRB5CCNAME not defined in HTTP request environment')
+            return self.marshal(None, CCacheError())
+        try:
+            self.create_context(ccache=user_ccache)
+            response = super(xmlserver, self).__call__(environ, start_response)
+            if getattr(context, 'session_data', None) is None and \
+              self.env.context != 'lite':
+                self.finalize_kerberos_acquisition('xmlserver', user_ccache, environ, start_response)
+        except PublicError, e:
+            status = HTTP_STATUS_SUCCESS
+            response = status
+            headers = [('Content-Type', 'text/plain; charset=utf-8')]
+            start_response(status, headers)
+            return self.marshal(None, e)
+        finally:
+            destroy_context()
+        return response
+
+    def listMethods(self, *params):
+        return tuple(name.decode('UTF-8') for name in self.Command)
+
+    def methodSignature(self, *params):
+        return u'methodSignature not implemented'
+
+    def methodHelp(self, *params):
+        return u'methodHelp not implemented'
+
+    def unmarshal(self, data):
+        (params, name) = xml_loads(data)
+        (args, options) = params_2_args_options(params)
+        return (name, args, options, None)
+
+    def marshal(self, result, error, _id=None):
+        if error:
+            self.debug('response: %s: %s', error.__class__.__name__, str(error))
+            response = Fault(error.errno, error.strerror)
+        else:
+            if isinstance(result, dict):
+                self.debug('response: entries returned %d', result.get('count', 1))
+            response = (result,)
+        return xml_dumps(response, methodresponse=True)
+
+
 class jsonserver_session(jsonserver, KerberosSession):
     """
     JSON RPC server protected with session auth.
@@ -992,3 +996,96 @@ class login_password(Backend, KerberosSession, HTTP_Status):
         if returncode != 0:
             raise InvalidSessionPassword(principal=principal, message=unicode(stderr))
 
+
+class xmlserver_session(xmlserver, KerberosSession):
+    """
+    XML RPC server protected with session auth.
+    """
+
+    key = '/session/xml'
+
+    def __init__(self):
+        super(xmlserver_session, self).__init__()
+        auth_mgr = AuthManagerKerb(self.__class__.__name__)
+        session_mgr.auth_mgr.register(auth_mgr.name, auth_mgr)
+
+    def _on_finalize(self):
+        super(xmlserver_session, self)._on_finalize()
+        self.kerb_session_on_finalize()
+
+    def need_login(self, start_response):
+        status = '401 Unauthorized'
+        headers = []
+        response = ''
+
+        self.debug('xmlserver_session: %s need login', status)
+
+        start_response(status, headers)
+        return [response]
+
+    def __call__(self, environ, start_response):
+        '''
+        '''
+
+        self.debug('WSGI xmlserver_session.__call__:')
+
+        # Load the session data
+        session_data = session_mgr.load_session_data(environ.get('HTTP_COOKIE'))
+        session_id = session_data['session_id']
+
+        self.debug('xmlserver_session.__call__: session_id=%s start_timestamp=%s access_timestamp=%s expiration_timestamp=%s',
+                   session_id,
+                   fmt_time(session_data['session_start_timestamp']),
+                   fmt_time(session_data['session_access_timestamp']),
+                   fmt_time(session_data['session_expiration_timestamp']))
+
+        ccache_data = session_data.get('ccache_data')
+
+        # Redirect to /ipa/xml if no Kerberos credentials
+        if ccache_data is None:
+            self.debug('xmlserver_session.__call_: no ccache, need TGT')
+            return self.need_login(start_response)
+
+        ipa_ccache_name = bind_ipa_ccache(ccache_data)
+
+        # Redirect to /ipa/xml if Kerberos credentials are expired
+        cc = KRB5_CCache(ipa_ccache_name)
+        if not cc.valid(self.api.env.host, self.api.env.realm):
+            self.debug('xmlserver_session.__call_: ccache expired, deleting session, need login')
+            # The request is finished with the ccache, destroy it.
+            release_ipa_ccache(ipa_ccache_name)
+            return self.need_login(start_response)
+
+        # Update the session expiration based on the Kerberos expiration
+        endtime = cc.endtime(self.api.env.host, self.api.env.realm)
+        self.update_session_expiration(session_data, endtime)
+
+        # Store the session data in the per-thread context
+        setattr(context, 'session_data', session_data)
+
+        environ['KRB5CCNAME'] = ipa_ccache_name
+
+        try:
+            response = super(xmlserver_session, self).__call__(environ, start_response)
+        finally:
+            # Kerberos may have updated the ccache data during the
+            # execution of the command therefore we need refresh our
+            # copy of it in the session data so the next command sees
+            # the same state of the ccache.
+            #
+            # However we must be careful not to restore the ccache
+            # data in the session data if it was explicitly deleted
+            # during the execution of the command. For example the
+            # logout command removes the ccache data from the session
+            # data to invalidate the session credentials.
+
+            if session_data.has_key('ccache_data'):
+                session_data['ccache_data'] = load_ccache_data(ipa_ccache_name)
+
+            # The request is finished with the ccache, destroy it.
+            release_ipa_ccache(ipa_ccache_name)
+            # Store the session data.
+            session_mgr.store_session_data(session_data)
+            destroy_context()
+
+        return response
diff --git a/tests/test_ipapython/test_keyring.py b/tests/test_ipapython/test_keyring.py
new file mode 100644
index 0000000000000000000000000000000000000000..568fd5ee1b507dfc5ff2ff344f473c6b28763373
--- /dev/null
+++ b/tests/test_ipapython/test_keyring.py
@@ -0,0 +1,147 @@
+# Authors:
+#   Rob Crittenden <rcrit...@redhat.com>
+#
+# Copyright (C) 2012  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/>.
+"""
+Test the `kernel_keyring.py` module.
+"""
+
+from nose.tools import raises, assert_raises  # pylint: disable=E0611
+from ipapython import kernel_keyring
+
+TEST_KEY = 'ipa_test'
+TEST_VALUE = 'abc123'
+UPDATE_VALUE = '123abc'
+
+SIZE_256 = 'abcdefgh' * 32
+SIZE_512 = 'abcdefgh' * 64
+SIZE_1024 = 'abcdefgh' * 128
+
+class test_keyring(object):
+    """
+    Test the kernel keyring interface
+    """
+
+    def setUp(self):
+        try:
+            kernel_keyring.del_key(TEST_KEY)
+        except ValueError:
+            pass
+        try:
+            kernel_keyring.del_key(SIZE_256)
+        except ValueError:
+            pass
+
+    def test_01(self):
+        """
+        Add a new key and value, then remove it
+        """
+        kernel_keyring.add_key(TEST_KEY, TEST_VALUE)
+        result = kernel_keyring.read_key(TEST_KEY)
+        assert(result == TEST_VALUE)
+
+        kernel_keyring.del_key(TEST_KEY)
+
+        # Make sure it is gone
+        try:
+            result = kernel_keyring.read_key(TEST_KEY)
+        except ValueError, e:
+            assert e.message == 'key %s not found' % TEST_KEY
+
+    def test_02(self):
+        """
+        Delete a non_existent key
+        """
+        try:
+            kernel_keyring.del_key(TEST_KEY)
+            raise AssertionError('key should not have been deleted')
+        except ValueError:
+            pass
+
+    @raises(ValueError)
+    def test_03(self):
+        """
+        Add a duplicate key
+        """
+        kernel_keyring.add_key(TEST_KEY, TEST_VALUE)
+        kernel_keyring.add_key(TEST_KEY, TEST_VALUE)
+
+    def test_04(self):
+        """
+        Update the value in a key
+        """
+        kernel_keyring.update_key(TEST_KEY, UPDATE_VALUE)
+        result = kernel_keyring.read_key(TEST_KEY)
+        assert(result == UPDATE_VALUE)
+
+        # Now update it 10 times
+        for i in xrange(10):
+            kernel_keyring.update_key(TEST_KEY, 'test %d' %  i)
+            result = kernel_keyring.read_key(TEST_KEY)
+            assert(result == 'test %d' % i)
+
+        kernel_keyring.del_key(TEST_KEY)
+
+    @raises(ValueError)
+    def test_05(self):
+        """
+        Read a non-existent key
+        """
+        result = kernel_keyring.read_key(TEST_KEY)
+
+    def test_06(self):
+        """
+        See if a key is available
+        """
+        kernel_keyring.add_key(TEST_KEY, TEST_VALUE)
+
+        result = kernel_keyring.has_key(TEST_KEY)
+        assert(result == True)
+        kernel_keyring.del_key(TEST_KEY)
+
+        result = kernel_keyring.has_key(TEST_KEY)
+        assert(result == False)
+
+    def test_07(self):
+        """
+        Test a 256-byte key
+        """
+        kernel_keyring.add_key(SIZE_256, TEST_VALUE)
+        result = kernel_keyring.read_key(SIZE_256)
+        assert(result == TEST_VALUE)
+
+        kernel_keyring.del_key(SIZE_256)
+
+    def test_08(self):
+        """
+        Test 512-bytes of data
+        """
+        kernel_keyring.add_key(TEST_KEY, SIZE_512)
+        result = kernel_keyring.read_key(TEST_KEY)
+        assert(result == SIZE_512)
+
+        kernel_keyring.del_key(TEST_KEY)
+
+    def test_09(self):
+        """
+        Test 1k bytes of data
+        """
+        kernel_keyring.add_key(TEST_KEY, SIZE_1024)
+        result = kernel_keyring.read_key(TEST_KEY)
+        assert(result == SIZE_1024)
+
+        kernel_keyring.del_key(TEST_KEY)
-- 
1.7.10.1

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to