diff options
Diffstat (limited to 'ipaserver/rpcserver.py')
-rw-r--r-- | ipaserver/rpcserver.py | 253 |
1 files changed, 161 insertions, 92 deletions
diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py index 205dc7655..d0bb605f5 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) |