summaryrefslogtreecommitdiffstats
path: root/ipaserver/rpcserver.py
diff options
context:
space:
mode:
authorJohn Dennis <jdennis@redhat.com>2012-02-15 10:26:42 -0500
committerRob Crittenden <rcritten@redhat.com>2012-02-27 05:54:58 -0500
commit49289bca5370b4b74f9bf4e5fd052d298a2a94d1 (patch)
treef152cbd821e01adc94230302571e1a8d97dbb64e /ipaserver/rpcserver.py
parent85462d063453f8614b63eddbba568fed034b0037 (diff)
downloadfreeipa.git-49289bca5370b4b74f9bf4e5fd052d298a2a94d1.tar.gz
freeipa.git-49289bca5370b4b74f9bf4e5fd052d298a2a94d1.tar.xz
freeipa.git-49289bca5370b4b74f9bf4e5fd052d298a2a94d1.zip
Tweak the session auth to reflect developer consensus.
* 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. https://fedorahosted.org/freeipa/ticket/2362
Diffstat (limited to 'ipaserver/rpcserver.py')
-rw-r--r--ipaserver/rpcserver.py253
1 files changed, 161 insertions, 92 deletions
diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py
index 205dc765..d0bb605f 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,6 +316,10 @@ class xmlserver(WSGIExecutioner):
'''
self.debug('WSGI xmlserver.__call__:')
+ ccache=environ.get('KRB5CCNAME')
+ if ccache is None:
+ return self.marshal(None, CCacheError())
+ self.create_context(ccache=ccache)
try:
self.create_context(ccache=environ.get('KRB5CCNAME'))
response = super(xmlserver, self).__call__(environ, start_response)
@@ -339,23 +342,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)
@@ -489,17 +475,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):
'''
@@ -507,51 +482,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):
@@ -606,15 +537,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:
@@ -652,8 +722,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
@@ -673,7 +742,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)