diff options
Diffstat (limited to 'ipaserver/rpcserver.py')
-rw-r--r-- | ipaserver/rpcserver.py | 172 |
1 files changed, 166 insertions, 6 deletions
diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py index 955c11b7f..a2fd64149 100644 --- a/ipaserver/rpcserver.py +++ b/ipaserver/rpcserver.py @@ -30,13 +30,17 @@ from ipalib.backend import Executioner from ipalib.errors import PublicError, InternalError, CommandError, JSONError, ConversionError, CCacheError, RefererError from ipalib.request import context, Connection, destroy_context from ipalib.rpc import xml_dumps, xml_loads -from ipalib.util import make_repr +from ipalib.util import make_repr, parse_time_duration from ipalib.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.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 wsgiref.util import shift_path_info from ipapython.version import VERSION import base64 import os import string +import datetime from decimal import Decimal _not_found_template = """<html> <head> @@ -139,8 +143,8 @@ class session(Executioner): return key in self.__apps def __call__(self, environ, start_response): + self.debug('WSGI session.__call__:') try: - self.create_context(ccache=environ.get('KRB5CCNAME')) return self.route(environ, start_response) finally: destroy_context() @@ -200,9 +204,7 @@ class WSGIExecutioner(Executioner): name = None args = () options = {} - if not 'KRB5CCNAME' in environ: - return self.marshal(result, CCacheError(), _id) - self.debug('Request environment: %s' % environ) + if not 'HTTP_REFERER' in environ: return self.marshal(result, RefererError(referer='missing'), _id) if not environ['HTTP_REFERER'].startswith('https://%s/ipa' % self.api.env.host) and not self.env.in_tree: @@ -263,15 +265,25 @@ class WSGIExecutioner(Executioner): """ WSGI application for execution. """ + + self.debug('WSGI WSGIExecutioner.__call__:') try: status = '200 OK' response = self.wsgi_execute(environ) headers = [('Content-Type', self.content_type + '; charset=utf-8')] except StandardError, e: - self.exception('%s.__call__():', self.name) + self.exception('WSGI %s.__call__():', self.name) status = '500 Internal Server Error' response = status headers = [('Content-Type', 'text/plain')] + + session_data = getattr(context, 'session_data', None) + if session_data is not None: + # Send session cookie back and store session data + # FIXME: the URL path should be retreived from somewhere (but where?), not hardcoded + session_cookie = session_mgr.generate_cookie('/ipa', session_data['session_id']) + headers.append(('Set-Cookie', session_cookie)) + start_response(status, headers) return [response] @@ -300,6 +312,18 @@ class xmlserver(WSGIExecutioner): } super(xmlserver, self)._on_finalize() + def __call__(self, environ, start_response): + ''' + ''' + + self.debug('WSGI xmlserver.__call__:') + self.create_context(ccache=environ.get('KRB5CCNAME')) + try: + response = super(xmlserver, self).__call__(environ, start_response) + finally: + destroy_context() + return response + def listMethods(self, *params): return tuple(name.decode('UTF-8') for name in self.Command) @@ -461,6 +485,69 @@ 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): + ''' + ''' + + 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() + + return response + def marshal(self, result, error, _id=None): if error: assert isinstance(error, PublicError) @@ -512,3 +599,76 @@ class jsonserver(WSGIExecutioner): ) options = dict((str(k), v) for (k, v) in options.iteritems()) return (method, args, options, _id) + +class krblogin(Backend): + 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) + + # Set the session expiration time + try: + seconds = parse_time_duration(self.api.env.session_auth_duration) + self.session_auth_duration = int(seconds) + self.debug("session_auth_duration: %s", datetime.timedelta(seconds=self.session_auth_duration)) + except Exception, e: + self.session_auth_duration = default_max_session_lifetime + self.error('unable to parse session_auth_duration, defaulting to %d: %s', + self.session_auth_duration, e) + + + def __call__(self, environ, start_response): + headers = [] + + self.debug('WSGI krblogin.__call__:') + + # Get the ccache created by mod_auth_kerb + ccache=environ.get('KRB5CCNAME') + if ccache is None: + status = '500 Internal Error' + response = 'KRB5CCNAME not defined' + start_response(status, headers) + return [response] + + ccache_scheme, ccache_location = krb5_parse_ccache(ccache) + assert ccache_scheme == 'FILE' + + # Retrieve the session data (or newly create) + session_data = session_mgr.load_session_data(environ.get('HTTP_COOKIE')) + session_id = session_data['session_id'] + + # Copy the ccache file contents into the session data + session_data['ccache_data'] = read_krbccache_file(ccache_location) + + # 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) + + # Account for clock skew and/or give us some time leeway + krb_expiration = endtime - krb_ticket_expiration_threshold + + # Set the session expiration time + session_mgr.set_session_expiration_time(session_data, + lifetime=self.session_auth_duration, + max_age=krb_expiration) + + # Store the session data now that it's been updated with the ccache + session_mgr.store_session_data(session_data) + + self.debug('krblogin: ccache="%s" session_id="%s" ccache="%s"', + ccache, session_id, ccache) + + # Return success and set session cookie + status = '200 Success' + response = '' + + session_cookie = session_mgr.generate_cookie('/ipa', session_data['session_id']) + headers.append(('Set-Cookie', session_cookie)) + + start_response(status, headers) + return [response] |