[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]

[Freeipa-devel] [PATCH 62] Tweak the session auth to reflect developer consensus.




--
John Dennis <jdennis redhat com>

Looking to carve out IT costs?
www.redhat.com/carveoutcosts/
>From af0bf4f002b4aa7c2eae8907909d5396c6e80a7b Mon Sep 17 00:00:00 2001
From: John Dennis <jdennis redhat com>
Date: Wed, 15 Feb 2012 10:26:42 -0500
Subject: [PATCH 62] Tweak the session auth to reflect developer consensus.
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 8bit

* Increase the session ID from 48 random bits to 128.

* Implement the sesison_logout RPC command. It permits the UI to send
  a command that destroys the users credentials in the current
  session.

* Restores the original web URL's and their authentication
  protections. Adds a new URL for sessions /ipa/session/json. Restores
  the original Kerberos auth which was for /ipa and everything
  below. New /ipa/session/json URL is treated as an exception and
  turns all authenticaion off. Similar to how /ipa/ui is handled.

* Refactor the RPC handlers in rpcserver.py such that there is one
  handler per URL, specifically one handler per RPC and AuthMechanism
  combination.

* Reworked how the URL names are used to map a URL to a
  handler. Previously it only permitted one level in the URL path
  hierarchy. We now dispatch on more that one URL path component.

* Renames the api.Backend.session object to wsgi_dispatch. The use of
  the name session was historical and is now confusing since we've
  implemented sessions in a different location than the
  api.Backend.session object, which is really a WSGI dispatcher, hence
  the new name wsgi_dispatch.

* Bullet-proof the setting of the KRB5CCNAME environment
  variable. ldap2.connect already sets it via the create_context()
  call but just in case that's not called or not called early enough
  (we now have other things besides ldap which need the ccache) we
  explicitly set it early as soon as we know it.

* Rework how we test for credential validity and expiration. The
  previous code did not work with s4u2proxy because it assumed the
  existance of a TGT. Now we first try ldap credentials and if we
  can't find those fallback to the TGT. This logic was moved to the
  KRB5_CCache object, it's an imperfect location for it but it's the
  only location that makes sense at the moment given some of the
  current code limitations. The new methods are KRB5_CCache.valid()
  and KRB5_CCache.endtime().

* Add two new classes to session.py AuthManager and
  SessionAuthManager. Their purpose is to emit authication events to
  interested listeners. At the moment the logout event is the only
  event, but the framework should support other events as they arise.

* Add BuildRequires python-memcached to freeipa.spec.in

* Removed the marshaled_dispatch method, it was cruft, no longer
  referenced.
---
 freeipa.spec.in                        |    1 +
 install/conf/ipa.conf                  |   23 +--
 install/share/wsgi.py                  |    3 +-
 install/ui/ipa.js                      |    3 +-
 ipalib/backend.py                      |    5 +
 ipalib/krb_utils.py                    |   71 +++++++++-
 ipalib/session.py                      |  126 +++++++++++++++-
 ipaserver/plugins/xmlserver.py         |    7 +-
 ipaserver/rpcserver.py                 |  254 ++++++++++++++++++++------------
 lite-server.py                         |    2 +-
 make-lint                              |    2 +
 tests/test_ipaserver/test_rpcserver.py |    6 +-
 12 files changed, 376 insertions(+), 127 deletions(-)

diff --git a/freeipa.spec.in b/freeipa.spec.in
index e619855..729b543 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -73,6 +73,7 @@ BuildRequires:  python-rhsm
 BuildRequires:  pyOpenSSL
 BuildRequires:  pylint
 BuildRequires:  libipa_hbac-python
+BuildRequires:  python-memcached
 
 %description
 IPA is an integrated solution to provide centrally managed Identity (machine,
diff --git a/install/conf/ipa.conf b/install/conf/ipa.conf
index 676086a..cd806be 100644
--- a/install/conf/ipa.conf
+++ b/install/conf/ipa.conf
@@ -44,8 +44,8 @@ WSGIScriptReloading Off
 
 KrbConstrainedDelegationLock ipa
 
-# Protect UI login url with Kerberos
-<Location "/ipa/login">
+# Protect /ipa and everything below it in webspace with Apache Kerberos auth
+<Location "/ipa">
   AuthType Kerberos
   AuthName "Kerberos Login"
   KrbMethodNegotiate on
@@ -59,22 +59,13 @@ KrbConstrainedDelegationLock ipa
   ErrorDocument 401 /ipa/errors/unauthorized.html
 </Location>
 
-# Protect xmlrpc url with Kerberos
-<Location "/ipa/xml">
-  AuthType Kerberos
-  AuthName "Kerberos Login"
-  KrbMethodNegotiate on
-  KrbMethodK5Passwd off
-  KrbServiceName HTTP
-  KrbAuthRealms $REALM
-  Krb5KeyTab /etc/httpd/conf/ipa.keytab
-  KrbSaveCredentials on
-  KrbConstrainedDelegation on
-  Require valid-user
-  ErrorDocument 401 /ipa/errors/unauthorized.html
+# Turn off Apache authentication for sessions
+<Location "/ipa/session">
+  Satisfy Any
+  Order Deny,Allow
+  Allow from all
 </Location>
 
-
 # This is where we redirect on failed auth
 Alias /ipa/errors "/usr/share/ipa/html"
 
diff --git a/install/share/wsgi.py b/install/share/wsgi.py
index b61b919..9f7d3f4 100644
--- a/install/share/wsgi.py
+++ b/install/share/wsgi.py
@@ -1,6 +1,7 @@
 # Authors:
 #   Rob Crittenden <rcritten redhat com>
 #   Jason Gerard DeRose <jderose redhat com>
+#   John Dennis <jdennis redhat com>
 #
 # Copyright (C) 2010  Red Hat
 # see file 'COPYING' for use and warranty information
@@ -45,6 +46,6 @@ else:
     # This is the WSGI callable:
     def application(environ, start_response):
         if not environ['wsgi.multithread']:
-            return api.Backend.session(environ, start_response)
+            return api.Backend.wsgi_dispatch(environ, start_response)
         else:
             api.log.error("IPA does not work with the threaded MPM, use the pre-fork MPM")
diff --git a/install/ui/ipa.js b/install/ui/ipa.js
index 82e8920..a599f6a 100644
--- a/install/ui/ipa.js
+++ b/install/ui/ipa.js
@@ -3,6 +3,7 @@
  *    Pavel Zuna <pzuna redhat com>
  *    Adam Young <ayoung redhat com>
  *    Endi Dewata <edewata redhat com>
+ *    John Dennis <jdennis redhat com>
  *
  * Copyright (C) 2010 Red Hat
  * see file 'COPYING' for use and warranty information
@@ -58,7 +59,7 @@ var IPA = function() {
 
         // if current path matches live server path, use live data
         if (that.url && window.location.pathname.substring(0, that.url.length) === that.url) {
-            that.json_url = params.url || '/ipa/json';
+            that.json_url = params.url || '/ipa/session/json';
             that.login_url = params.url || '/ipa/login';
 
         } else { // otherwise use fixtures
diff --git a/ipalib/backend.py b/ipalib/backend.py
index 7ed378e..0232fa5 100644
--- a/ipalib/backend.py
+++ b/ipalib/backend.py
@@ -23,6 +23,7 @@ Base classes for all backed-end plugins.
 
 import threading
 import plugable
+import os
 from errors import PublicError, InternalError, CommandError
 from request import context, Connection, destroy_context
 
@@ -106,6 +107,10 @@ class Executioner(Backend):
         """
         client_ip: The IP address of the remote client.
         """
+
+        if ccache is not None:
+            os.environ["KRB5CCNAME"] = ccache
+
         if self.env.in_server:
             self.Backend.ldap2.connect(ccache=ccache)
         else:
diff --git a/ipalib/krb_utils.py b/ipalib/krb_utils.py
index e04c70a..21bca68 100644
--- a/ipalib/krb_utils.py
+++ b/ipalib/krb_utils.py
@@ -158,7 +158,6 @@ class KRB5_CCache(object):
         self.ccache = None
         self.principal = None
 
-        self.debug('opening ccache file "%s"', ccache)
         try:
             self.context = krbV.default_context()
             self.scheme, self.name = krb5_parse_ccache(ccache)
@@ -228,8 +227,6 @@ class KRB5_CCache(object):
         except krbV.Krb5Error, e:
             error_code = e.args[0]
             if error_code == KRB5_CC_NOTFOUND:
-                self.debug('"%s" credential not found in "%s" ccache',
-                           krbV_principal.name, self.ccache_str()) #pylint: disable=E1103
                 raise KeyError('"%s" credential not found in "%s" ccache' % \
                                (krbV_principal.name, self.ccache_str())) #pylint: disable=E1103
             raise e
@@ -281,7 +278,7 @@ class KRB5_CCache(object):
             cred = self.get_credentials(krbV_principal)
             authtime, starttime, endtime, renew_till = cred[3]
 
-            self.debug('principal=%s, authtime=%s, starttime=%s, endtime=%s, renew_till=%s',
+            self.debug('get_credential_times: principal=%s, authtime=%s, starttime=%s, endtime=%s, renew_till=%s',
                        krbV_principal.name, #pylint: disable=E1103
                        krb5_format_time(authtime), krb5_format_time(starttime),
                        krb5_format_time(endtime), krb5_format_time(renew_till))
@@ -327,3 +324,69 @@ class KRB5_CCache(object):
         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('ldap', 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('ldap', 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('"%s" ccache endtime=%s', self.ccache_str(), krb5_format_time(result))
+        return result
diff --git a/ipalib/session.py b/ipalib/session.py
index a586439..1f5ee37 100644
--- a/ipalib/session.py
+++ b/ipalib/session.py
@@ -25,6 +25,8 @@ import re
 import time
 from text import _
 from ipapython.ipa_log_manager import *
+from ipalib import api, errors
+from ipalib import Command
 from ipalib.krb_utils import *
 
 __doc__ = '''
@@ -632,6 +634,82 @@ def fmt_time(timestamp):
 
 #-------------------------------------------------------------------------------
 
+class AuthManager(object):
+    '''
+    This class is an abstract base class and is meant to be subclassed
+    to provide actual functionality. The purpose is to encapsulate all
+    the callbacks one might need to manage authenticaion. Different
+    authentication mechanisms will instantiate a subclass of this and
+    register it with the SessionAuthManger. When an authentication
+    event occurs the matching method will be called for each
+    registered class. This allows the SessionAuthManager to notify
+    interested parties.
+    '''
+
+    def __init__(self, name):
+        log_mgr.get_logger(self, True)
+        self.name = name
+
+
+    def logout(self, session_data):
+        '''
+        Called when a user requests to be logged out of their session.
+
+        :parameters:
+          session_data
+            The current session data
+        :returns:
+          None
+        '''
+        self.debug('AuthManager.logout.%s:', self.name)
+
+class SessionAuthManager(object):
+    '''
+    '''
+
+    def __init__(self):
+        '''
+        '''
+        log_mgr.get_logger(self, True)
+        self.auth_managers = {}
+
+    def register(self, name, auth_mgr):
+        self.debug('SessionAuthManager.register: name=%s', name)
+
+        existing_mgr = self.auth_managers.get(name)
+        if existing_mgr is not None:
+            raise KeyError('cannot register auth manager named "%s" one already exists, name="%s" object=%s',
+                           name, existing_mgr.name, repr(existing_mgr))
+
+        if not isinstance(auth_mgr, AuthManager):
+            raise TypeError('auth_mgr must be an instance of AuthManager, not %s',
+                            auth_mgr.__class__.__name__)
+
+        self.auth_managers[name] = auth_mgr
+
+
+    def unregister(self, name):
+        self.debug('SessionAuthManager.unregister: name=%s', name)
+
+        if not self.auth_managers.has_key(name):
+            raise KeyError('cannot unregister auth manager named "%s", does not exist',
+                           name)
+        del self.auth_managers[name]
+
+
+    def logout(self, session_data):
+        '''
+        '''
+        self.debug('SessionAuthManager.logout:')
+
+        for auth_mgr in self.auth_managers.values():
+            try:
+                auth_mgr.logout(session_data)
+            except Exception, e:
+                self.error('%s auth_mgr logout failed: %s', auth_mgr.name, e)
+
+#-------------------------------------------------------------------------------
+
 class SessionManager(object):
 
     '''
@@ -649,8 +727,9 @@ class SessionManager(object):
 
         log_mgr.get_logger(self, True)
         self.generated_session_ids = set()
+        self.auth_mgr = SessionAuthManager()
 
-    def generate_session_id(self, n_bits=48):
+    def generate_session_id(self, n_bits=128):
         '''
         Return a random string to be used as a session id.
 
@@ -790,8 +869,7 @@ class MemcacheSessionManager(SessionManager):
         n_retries = 0
         while n_retries < max_retries:
             session_id = super(MemcacheSessionManager, self).new_session_id(max_retries)
-            session_key = self.session_key(session_id)
-            session_data = self.mc.get(session_key)
+            session_data = self.get_session_data(session_id)
             if session_data is None:
                 break
             n_retries += 1
@@ -843,6 +921,21 @@ class MemcacheSessionManager(SessionManager):
         '''
         return 'ipa.session.%s' % (session_id)
 
+    def get_session_data(self, session_id):
+        '''
+        Given a session id retrieve the session data associated with it.
+        If no session data exists for the session id return None.
+
+        :parameters:
+          session_id
+            The session id whose session data is desired.
+        :returns:
+          Session data if found, None otherwise.
+        '''
+        session_key = self.session_key(session_id)
+        session_data = self.mc.get(session_key)
+        return session_data
+
     def get_session_id_from_http_cookie(self, cookie_header):
         '''
         Parse an HTTP cookie header and search for our session
@@ -904,8 +997,7 @@ class MemcacheSessionManager(SessionManager):
             self.store_session_data(session_data)
             return session_data
         else:
-            session_key = self.session_key(session_id)
-            session_data = self.mc.get(session_key)
+            session_data = self.get_session_data(session_id)
             if session_data is None:
                 self.debug('no session data in cache with id=%s, generating empty session data', session_id)
                 session_data = self.new_session_data(session_id)
@@ -1094,5 +1186,29 @@ def delete_krbccache_file(krbccache_pathname=None):
 
 #-------------------------------------------------------------------------------
 
+from ipalib.request import context
+
+class session_logout(Command):
+    '''
+    RPC command used to log the current user out of their session.
+    '''
+
+    def execute(self, *args, **options):
+        session_data = getattr(context, 'session_data', None)
+        if session_data is None:
+            self.debug('session logout command: no session_data found')
+        else:
+            session_id = session_data.get('session_id')
+            self.debug('session logout command: session_id=%s', session_id)
+
+            # Notifiy registered listeners
+            session_mgr.auth_mgr.logout(session_data)
+
+        return dict(result=None)
+
+api.register(session_logout)
+
+#-------------------------------------------------------------------------------
+
 
 session_mgr = MemcacheSessionManager()
diff --git a/ipaserver/plugins/xmlserver.py b/ipaserver/plugins/xmlserver.py
index 03bca9a..d2a28ec 100644
--- a/ipaserver/plugins/xmlserver.py
+++ b/ipaserver/plugins/xmlserver.py
@@ -25,8 +25,9 @@ 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 session, xmlserver, jsonserver, krblogin
-    api.register(session)
+    from ipaserver.rpcserver import wsgi_dispatch, xmlserver, jsonserver_kerb, jsonserver_session, krblogin
+    api.register(wsgi_dispatch)
     api.register(xmlserver)
-    api.register(jsonserver)
+    api.register(jsonserver_kerb)
+    api.register(jsonserver_session)
     api.register(krblogin)
diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py
index 91e525a..db552dc 100644
--- a/ipaserver/rpcserver.py
+++ b/ipaserver/rpcserver.py
@@ -32,9 +32,9 @@ from ipalib.request import context, Connection, destroy_context
 from ipalib.rpc import xml_dumps, xml_loads
 from ipalib.util import make_repr, parse_time_duration
 from ipapython.compat import json
-from ipalib.session import session_mgr, read_krbccache_file, store_krbccache_file, delete_krbccache_file, fmt_time, default_max_session_lifetime
+from ipalib.session import session_mgr, AuthManager, read_krbccache_file, store_krbccache_file, delete_krbccache_file, fmt_time, default_max_session_lifetime
 from ipalib.backend import Backend
-from ipalib.krb_utils import krb5_parse_ccache, KRB5_CCache, krb5_format_tgt_principal_name, krb5_format_service_principal_name, krb_ticket_expiration_threshold
+from ipalib.krb_utils import krb5_parse_ccache, KRB5_CCache, krb_ticket_expiration_threshold
 from wsgiref.util import shift_path_info
 from ipapython.version import VERSION
 import base64
@@ -118,18 +118,17 @@ def extract_query(environ):
     return query
 
 
-class session(Executioner):
+class wsgi_dispatch(Executioner):
     """
     WSGI routing middleware and entry point into IPA server.
 
-    The `session` plugin is the entry point into the IPA server.  It will create
-    an LDAP connection (from a session cookie or the KRB5CCNAME header) and then
-    dispatch the request to the appropriate application.  In WSGI parlance,
-    `session` is *middleware*.
+    The `wsgi_dispatch` plugin is the entry point into the IPA server.
+    It dispatchs the request to the appropriate wsgi application
+    handler which is specific to the authentication and RPC mechanism.
     """
 
     def __init__(self):
-        super(session, self).__init__()
+        super(wsgi_dispatch, self).__init__()
         self.__apps = {}
 
     def __iter__(self):
@@ -143,7 +142,7 @@ class session(Executioner):
         return key in self.__apps
 
     def __call__(self, environ, start_response):
-        self.debug('WSGI session.__call__:')
+        self.debug('WSGI wsgi_dispatch.__call__:')
         try:
             return self.route(environ, start_response)
         finally:
@@ -151,10 +150,10 @@ class session(Executioner):
 
     def _on_finalize(self):
         self.url = self.env['mount_ipa']
-        super(session, self)._on_finalize()
+        super(wsgi_dispatch, self)._on_finalize()
 
     def route(self, environ, start_response):
-        key = shift_path_info(environ)
+        key = environ.get('PATH_INFO')
         if key in self.__apps:
             app = self.__apps[key]
             return app(environ, start_response)
@@ -189,8 +188,8 @@ class WSGIExecutioner(Executioner):
 
     def set_api(self, api):
         super(WSGIExecutioner, self).set_api(api)
-        if 'session' in self.api.Backend:
-            self.api.Backend.session.mount(self, self.key)
+        if 'wsgi_dispatch' in self.api.Backend:
+            self.api.Backend.wsgi_dispatch.mount(self, self.key)
 
     def _on_finalize(self):
         self.url = self.env.mount_ipa + self.key
@@ -302,7 +301,7 @@ class xmlserver(WSGIExecutioner):
     """
 
     content_type = 'text/xml'
-    key = 'xml'
+    key = '/xml'
 
     def _on_finalize(self):
         self.__system = {
@@ -317,7 +316,10 @@ class xmlserver(WSGIExecutioner):
         '''
 
         self.debug('WSGI xmlserver.__call__:')
-        self.create_context(ccache=environ.get('KRB5CCNAME'))
+        ccache=environ.get('KRB5CCNAME')
+        if ccache is None:
+            return self.marshal(None, CCacheError())
+        self.create_context(ccache=ccache)
         try:
             response = super(xmlserver, self).__call__(environ, start_response)
         finally:
@@ -333,23 +335,6 @@ class xmlserver(WSGIExecutioner):
     def methodHelp(self, *params):
         return u'methodHelp not implemented'
 
-    def marshaled_dispatch(self, data, ccache, client_ip):
-        """
-        Execute the XML-RPC request contained in ``data``.
-        """
-        try:
-            self.create_context(ccache=ccache, client_ip=client_ip)
-            (params, name) = xml_loads(data)
-            if name in self.__system:
-                response = (self.__system[name](*params),)
-            else:
-                (args, options) = params_2_args_options(params)
-                response = (self.execute(name, *args, **options),)
-        except PublicError, e:
-            self.debug('response: %s: %s', e.__class__.__name__, str(e))
-            response = Fault(e.errno, e.strerror)
-        return xml_dumps(response, methodresponse=True)
-
     def unmarshal(self, data):
         (params, name) = xml_loads(data)
         (args, options) = params_2_args_options(params)
@@ -483,17 +468,6 @@ class jsonserver(WSGIExecutioner):
     """
 
     content_type = 'application/json'
-    key = 'json'
-
-    def need_login(self, start_response):
-        status = '401 Unauthorized'
-        headers = []
-        response = ''
-
-        self.debug('jsonserver: %s', status)
-
-        start_response(status, headers)
-        return [response]
 
     def __call__(self, environ, start_response):
         '''
@@ -501,51 +475,7 @@ class jsonserver(WSGIExecutioner):
 
         self.debug('WSGI jsonserver.__call__:')
 
-        # Load the session data
-        session_data = session_mgr.load_session_data(environ.get('HTTP_COOKIE'))
-        session_id = session_data['session_id']
-
-        self.debug('jsonserver.__call__: session_id=%s start_timestamp=%s write_timestamp=%s expiration_timestamp=%s',
-                   session_id,
-                   fmt_time(session_data['session_start_timestamp']),
-                   fmt_time(session_data['session_write_timestamp']),
-                   fmt_time(session_data['session_expiration_timestamp']))
-
-        ccache_data = session_data.get('ccache_data')
-
-        # Redirect to login if no Kerberos credentials
-        if ccache_data is None:
-            self.debug('no ccache, need login')
-            return self.need_login(start_response)
-
-        krbccache_pathname = store_krbccache_file(ccache_data)
-
-        # Redirect to login if Kerberos credentials are expired
-        cc = KRB5_CCache(krbccache_pathname)
-        ldap_principal = krb5_format_service_principal_name('ldap', self.api.env.host, self.api.env.realm)
-        tgt_principal = krb5_format_tgt_principal_name(self.api.env.realm)
-        if not (cc.credential_is_valid(ldap_principal) or cc.credential_is_valid(tgt_principal)):
-            self.debug('ccache expired, deleting session, need login')
-            session_mgr.delete_session_data(session_id)
-            delete_krbccache_file(krbccache_pathname)
-            return self.need_login(start_response)
-
-        # Store the session data in the per-thread context
-        setattr(context, 'session_data', session_data)
-
-        self.create_context(ccache=krbccache_pathname)
-
-        try:
-            response = super(jsonserver, self).__call__(environ, start_response)
-        finally:
-            # Kerberos may have updated the ccache data, refresh our copy of it
-            session_data['ccache_data'] = read_krbccache_file(krbccache_pathname)
-            # Delete the temporary ccache file we used
-            delete_krbccache_file(krbccache_pathname)
-            # Store the session data.
-            session_mgr.store_session_data(session_data)
-            destroy_context()
-
+        response = super(jsonserver, self).__call__(environ, start_response)
         return response
 
     def marshal(self, result, error, _id=None):
@@ -600,15 +530,154 @@ class jsonserver(WSGIExecutioner):
         options = dict((str(k), v) for (k, v) in options.iteritems())
         return (method, args, options, _id)
 
+class AuthManagerKerb(AuthManager):
+    '''
+    Instances of the AuthManger class are used to handle
+    authentication events delivered by the SessionManager. This class
+    specifcally handles the management of Kerbeos credentials which
+    may be stored in the session.
+    '''
+
+    def __init__(self, name):
+        super(AuthManagerKerb, self).__init__(name)
+
+    def logout(self, session_data):
+        '''
+        The current user has requested to be logged out. To accomplish
+        this we remove the user's kerberos credentials from their
+        session. This does not destroy the session, it just prevents
+        it from being used for fast authentication. Because the
+        credentials are no longer in the session cache any future
+        attempt will require the acquisition of credentials using one
+        of the login mechanisms.
+        '''
+
+        if session_data.has_key('ccache_data'):
+            self.debug('AuthManager.logout.%s: deleting ccache_data', self.name)
+            del session_data['ccache_data']
+        else:
+            self.error('AuthManager.logout.%s: session_data does not contain ccache_data', self.name)
+
+
+class jsonserver_session(jsonserver):
+    """
+    JSON RPC server protected with session auth.
+    """
+
+    key = '/session/json'
+
+    def __init__(self):
+        super(jsonserver_session, self).__init__()
+        auth_mgr = AuthManagerKerb(self.__class__.__name__)
+        session_mgr.auth_mgr.register(auth_mgr.name, auth_mgr)
+
+    def need_login(self, start_response):
+        status = '401 Unauthorized'
+        headers = []
+        response = ''
+
+        self.debug('jsonserver_session: %s', status)
+
+        start_response(status, headers)
+        return [response]
+
+    def __call__(self, environ, start_response):
+        '''
+        '''
+
+        self.debug('WSGI jsonserver_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('jsonserver_session.__call__: session_id=%s start_timestamp=%s write_timestamp=%s expiration_timestamp=%s',
+                   session_id,
+                   fmt_time(session_data['session_start_timestamp']),
+                   fmt_time(session_data['session_write_timestamp']),
+                   fmt_time(session_data['session_expiration_timestamp']))
+
+        ccache_data = session_data.get('ccache_data')
+
+        # Redirect to login if no Kerberos credentials
+        if ccache_data is None:
+            self.debug('no ccache, need login')
+            return self.need_login(start_response)
+
+        krbccache_pathname = store_krbccache_file(ccache_data)
+
+        # Redirect to login if Kerberos credentials are expired
+        cc = KRB5_CCache(krbccache_pathname)
+        if not cc.valid(self.api.env.host, self.api.env.realm):
+            self.debug('ccache expired, deleting session, need login')
+            delete_krbccache_file(krbccache_pathname)
+            return self.need_login(start_response)
+
+        # Store the session data in the per-thread context
+        setattr(context, 'session_data', session_data)
+
+        self.create_context(ccache=krbccache_pathname)
+
+        try:
+            response = super(jsonserver_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'] = read_krbccache_file(krbccache_pathname)
+
+            # Delete the temporary ccache file we used
+            delete_krbccache_file(krbccache_pathname)
+            # Store the session data.
+            session_mgr.store_session_data(session_data)
+            destroy_context()
+
+        return response
+
+class jsonserver_kerb(jsonserver):
+    """
+    JSON RPC server protected with kerberos auth.
+    """
+
+    key = '/json'
+
+    def __call__(self, environ, start_response):
+        '''
+        '''
+
+        self.debug('WSGI jsonserver_kerb.__call__:')
+
+        ccache=environ.get('KRB5CCNAME')
+        if ccache is None:
+            return self.marshal(None, CCacheError())
+        self.create_context(ccache=ccache)
+
+        try:
+            response = super(jsonserver_kerb, self).__call__(environ, start_response)
+        finally:
+            destroy_context()
+
+        return response
+
+
 class krblogin(Backend):
-    key = 'login'
+    key = '/login'
 
     def __init__(self):
         super(krblogin, self).__init__()
 
     def _on_finalize(self):
         super(krblogin, self)._on_finalize()
-        self.api.Backend.session.mount(self, self.key)
+        self.api.Backend.wsgi_dispatch.mount(self, self.key)
 
         # Set the session expiration time
         try:
@@ -646,8 +715,7 @@ class krblogin(Backend):
 
         # Compute when the session will expire
         cc = KRB5_CCache(ccache)
-        tgt_principal = krb5_format_tgt_principal_name(self.api.env.realm)
-        authtime, starttime, endtime, renew_till = cc.get_credential_times(tgt_principal)
+        endtime = cc.endtime(self.api.env.host, self.api.env.realm)
 
         # Account for clock skew and/or give us some time leeway
         krb_expiration = endtime - krb_ticket_expiration_threshold
@@ -667,7 +735,7 @@ class krblogin(Backend):
         status = '200 Success'
         response = ''
 
-        session_cookie = session_mgr.generate_cookie('/ipa', session_data['session_id'])
+        session_cookie = session_mgr.generate_cookie('/ipa', session_id)
         headers.append(('Set-Cookie', session_cookie))
 
         start_response(status, headers)
diff --git a/lite-server.py b/lite-server.py
index b7067a5..e065357 100755
--- a/lite-server.py
+++ b/lite-server.py
@@ -124,7 +124,7 @@ if __name__ == '__main__':
 
     urlmap = URLMap()
     apps = [
-        ('IPA', KRBCheater(api.Backend.session)),
+        ('IPA', KRBCheater(api.Backend.wsgi_dispatch)),
         ('webUI', KRBCheater(WebUIApp())),
     ]
     for (name, app) in apps:
diff --git a/make-lint b/make-lint
index 20f6281..94fb1ca 100755
--- a/make-lint
+++ b/make-lint
@@ -68,6 +68,8 @@ class IPATypeChecker(TypeChecker):
         'ipalib.parameters.File': ['stdin_if_missing'],
         'urlparse.SplitResult': ['netloc'],
         'ipalib.krb_utils.KRB5_CCache' : ['log', 'debug', 'info', 'warning', 'error', 'critical', 'exception'],
+        'ipalib.session.AuthManager' : ['log', 'debug', 'info', 'warning', 'error', 'critical', 'exception'],
+        'ipalib.session.SessionAuthManager' : ['log', 'debug', 'info', 'warning', 'error', 'critical', 'exception'],
         'ipalib.session.SessionManager' : ['log', 'debug', 'info', 'warning', 'error', 'critical', 'exception'],
         'ipalib.session.SessionCCache' : ['log', 'debug', 'info', 'warning', 'error', 'critical', 'exception'],
         'ipalib.session.MemcacheSessionManager' : ['log', 'debug', 'info', 'warning', 'error', 'critical', 'exception'],
diff --git a/tests/test_ipaserver/test_rpcserver.py b/tests/test_ipaserver/test_rpcserver.py
index 9aa16d0..e712078 100644
--- a/tests/test_ipaserver/test_rpcserver.py
+++ b/tests/test_ipaserver/test_rpcserver.py
@@ -84,7 +84,7 @@ def test_params_2_args_options():
 
 
 class test_session(object):
-    klass = rpcserver.session
+    klass = rpcserver.wsgi_dispatch
 
     def test_route(self):
         def app1(environ, start_response):
@@ -125,7 +125,7 @@ class test_session(object):
         # Test that StandardError is raise if trying override a mount:
         e = raises(StandardError, inst.mount, app2, 'foo')
         assert str(e) == '%s.mount(): cannot replace %r with %r at %r' % (
-            'session', app1, app2, 'foo'
+            'wsgi_dispatch', app1, app2, 'foo'
         )
 
         # Test mounting a second app:
@@ -141,7 +141,7 @@ class test_xmlserver(PluginTester):
 
     _plugin = rpcserver.xmlserver
 
-    def test_marshaled_dispatch(self):
+    def test_marshaled_dispatch(self): # FIXME
         (o, api, home) = self.instance('Backend', in_server=True)
 
 
-- 
1.7.7.6


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]