summaryrefslogtreecommitdiffstats
path: root/ipaserver/rpcserver.py
diff options
context:
space:
mode:
authorSimo Sorce <simo@redhat.com>2016-08-19 09:23:55 -0400
committerJan Cholasta <jcholast@redhat.com>2017-02-15 07:13:37 +0100
commitc894ebefc5c4c4c7ea340d6ddc4cd3c081917e4a (patch)
tree8511e93ca9e8e1df6c504b8f18d2fec733686d26 /ipaserver/rpcserver.py
parent11ef2cacbf2ebb67f80a0cf4a3e7b39da700188b (diff)
downloadfreeipa-c894ebefc5c4c4c7ea340d6ddc4cd3c081917e4a.tar.gz
freeipa-c894ebefc5c4c4c7ea340d6ddc4cd3c081917e4a.tar.xz
freeipa-c894ebefc5c4c4c7ea340d6ddc4cd3c081917e4a.zip
Change session handling
Stop using memcache, use mod_auth_gssapi filesystem based ccaches. Remove custom session handling, use mod_auth_gssapi and mod_session to establish and keep a session cookie. Add loopback to mod_auth_gssapi to do form absed auth and pass back a valid session cookie. And now that we do not remove ccaches files to move them to the memcache, we can avoid the risk of pollutting the filesystem by keeping a common ccache file for all instances of the same user. https://fedorahosted.org/freeipa/ticket/5959 Signed-off-by: Simo Sorce <simo@redhat.com> Reviewed-By: Jan Cholasta <jcholast@redhat.com>
Diffstat (limited to 'ipaserver/rpcserver.py')
-rw-r--r--ipaserver/rpcserver.py334
1 files changed, 113 insertions, 221 deletions
diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py
index 45550fb1f..2b1e42bf6 100644
--- a/ipaserver/rpcserver.py
+++ b/ipaserver/rpcserver.py
@@ -25,11 +25,10 @@ Also see the `ipalib.rpc` module.
from xml.sax.saxutils import escape
import os
-import datetime
import json
import traceback
import gssapi
-import time
+import requests
import ldap.controls
from pyasn1.type import univ, namedtype
@@ -51,22 +50,24 @@ from ipalib.errors import (PublicError, InternalError, JSONError,
from ipalib.request import context, destroy_context
from ipalib.rpc import (xml_dumps, xml_loads,
json_encode_binary, json_decode_binary)
-from ipalib.util import parse_time_duration, normalize_name
+from ipalib.util import normalize_name
from ipapython.dn import DN
from ipaserver.plugins.ldap2 import ldap2
from ipaserver.session import (
- get_session_mgr, AuthManager, get_ipa_ccache_name,
- load_ccache_data, bind_ipa_ccache, release_ipa_ccache, fmt_time,
- default_max_session_duration, krbccache_dir, krbccache_prefix)
+ get_ipa_ccache_name,
+ krbccache_dir, krbccache_prefix)
from ipalib.backend import Backend
from ipalib.krb_utils import (
- krb_ticket_expiration_threshold, krb5_format_principal_name,
- krb5_format_service_principal_name, get_credentials, get_credentials_if_valid)
+ krb5_format_principal_name,
+ krb5_format_service_principal_name, get_credentials_if_valid)
from ipapython import ipautil
from ipaplatform.paths import paths
from ipapython.version import VERSION
from ipalib.text import _
+from base64 import b64decode, b64encode
+from requests.auth import AuthBase
+
if six.PY3:
unicode = str
@@ -303,6 +304,7 @@ class WSGIExecutioner(Executioner):
Base class for execution backends with a WSGI application interface.
"""
+ headers = None
content_type = None
key = ''
@@ -424,22 +426,17 @@ class WSGIExecutioner(Executioner):
try:
status = HTTP_STATUS_SUCCESS
response = self.wsgi_execute(environ)
- headers = [('Content-Type', self.content_type + '; charset=utf-8')]
+ if self.headers:
+ headers = self.headers
+ else:
+ headers = [('Content-Type',
+ self.content_type + '; charset=utf-8')]
except Exception:
self.exception('WSGI %s.__call__():', self.name)
status = HTTP_STATUS_SERVER_ERROR
response = status.encode('utf-8')
headers = [('Content-Type', 'text/plain; charset=utf-8')]
- 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_mgr = get_session_mgr()
- session_cookie = session_mgr.generate_cookie('/ipa', session_data['session_id'],
- session_data['session_expiration_timestamp'])
- headers.append(('Set-Cookie', session_cookie))
-
start_response(status, headers)
return [response]
@@ -521,36 +518,73 @@ class jsonserver(WSGIExecutioner, HTTP_Status):
options = dict((str(k), v) for (k, v) in options.items())
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)
+class NegotiateAuth(AuthBase):
+ """Negotiate Augh using python GSSAPI"""
+ def __init__(self, target_host, ccache_name=None):
+ self.context = None
+ self.target_host = target_host
+ self.ccache_name = ccache_name
+
+ def __call__(self, request):
+ self.initial_step(request)
+ request.register_hook('response', self.handle_response)
+ return request
+
+ def deregister(self, response):
+ response.request.deregister_hook('response', self.handle_response)
+
+ def _get_negotiate_token(self, response):
+ token = None
+ if response is not None:
+ h = response.headers.get('www-authenticate', '')
+ if h.startswith('Negotiate'):
+ val = h[h.find('Negotiate') + len('Negotiate'):].strip()
+ if len(val) > 0:
+ token = b64decode(val)
+ return token
+
+ def _set_authz_header(self, request, token):
+ request.headers['Authorization'] = 'Negotiate ' + b64encode(token)
+
+ def initial_step(self, request, response=None):
+ if self.context is None:
+ store = {'ccache': self.ccache_name}
+ creds = gssapi.Credentials(usage='initiate', store=store)
+ name = gssapi.Name('HTTP@{0}'.format(self.target_host),
+ name_type=gssapi.NameType.hostbased_service)
+ self.context = gssapi.SecurityContext(creds=creds, name=name,
+ usage='initiate')
+
+ in_token = self._get_negotiate_token(response)
+ out_token = self.context.step(in_token)
+ self._set_authz_header(request, out_token)
+
+ def handle_response(self, response, **kwargs):
+ status = response.status_code
+ if status >= 400 and status != 401:
+ return response
+
+ in_token = self._get_negotiate_token(response)
+ if in_token is not None:
+ out_token = self.context.step(in_token)
+ if self.context.complete:
+ return response
+ elif not out_token:
+ return response
+
+ self._set_authz_header(response.request, out_token)
+ # use response so we can make another request
+ _ = response.content # pylint: disable=unused-variable
+ response.raw.release_conn()
+ newresp = response.connection.send(response.request, **kwargs)
+ newresp.history.append(response)
+ return self.handle_response(newresp, **kwargs)
- 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 'ccache_data' in session_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)
+ return response
-class KerberosSession(object):
+class KerberosSession(HTTP_Status):
'''
Functionally shared by all RPC handlers using both sessions and
Kerberos. This class must be implemented as a mixin class rather
@@ -558,101 +592,44 @@ class KerberosSession(object):
needing this do not share a common base class.
'''
- def kerb_session_on_finalize(self):
- '''
- Initialize values from the Env configuration.
-
- Why do it this way and not simply reference
- api.env.session_auth_duration? Because that config item cannot
- be used directly, it must be parsed and converted to an
- integer. It would be inefficient to reparse it on every
- request. So we parse it once and store the result in the class
- instance.
- '''
- # 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 as e:
- self.session_auth_duration = default_max_session_duration
- self.error('unable to parse session_auth_duration, defaulting to %d: %s',
- self.session_auth_duration, e)
-
- def update_session_expiration(self, session_data, krb_endtime):
- '''
- Each time a session is created or accessed we need to update
- it's expiration time. The expiration time is set inside the
- session_data.
-
- :parameters:
- session_data
- The session data whose expiration is being updatded.
- krb_endtime
- The UNIX timestamp for when the Kerberos credentials expire.
- :returns:
- None
- '''
-
- # Account for clock skew and/or give us some time leeway
- krb_expiration = krb_endtime - krb_ticket_expiration_threshold
-
- # Set the session expiration time
- session_mgr = get_session_mgr()
- session_mgr.set_session_expiration_time(session_data,
- duration=self.session_auth_duration,
- max_age=krb_expiration,
- duration_type=self.api.env.session_duration_type)
-
def finalize_kerberos_acquisition(self, who, ccache_name, environ, start_response, headers=None):
if headers is None:
headers = []
- # Retrieve the session data (or newly create)
- session_mgr = get_session_mgr()
- session_data = session_mgr.load_session_data(environ.get('HTTP_COOKIE'))
- session_id = session_data['session_id']
-
- self.debug('finalize_kerberos_acquisition: %s ccache_name="%s" session_id="%s"',
- who, ccache_name, session_id)
-
- # Copy the ccache file contents into the session data
- session_data['ccache_data'] = load_ccache_data(ccache_name)
-
- # Set when the session will expire
- creds = get_credentials(ccache_name=ccache_name)
- endtime = creds.lifetime + time.time()
- self.update_session_expiration(session_data, endtime)
-
- # Store the session data now that it's been updated with the ccache
- session_mgr.store_session_data(session_data)
-
- # The request is finished with the ccache, destroy it.
- release_ipa_ccache(ccache_name)
+ # Connect back to ourselves to get mod_auth_gssapi to
+ # generate a cookie for us.
+ try:
+ target = self.api.env.host
+ r = requests.get('http://{0}/ipa/session/cookie'.format(target),
+ auth=NegotiateAuth(target, ccache_name))
+ session_cookie = r.cookies.get("ipa_session")
+ if not session_cookie:
+ raise ValueError('No session cookie found')
+ except Exception as e:
+ return self.unauthorized(environ, start_response,
+ str(e),
+ 'Authentication failed')
- # Return success and set session cookie
- session_cookie = session_mgr.generate_cookie('/ipa', session_id,
- session_data['session_expiration_timestamp'])
- headers.append(('Set-Cookie', session_cookie))
+ headers.append(('IPASESSION', session_cookie))
start_response(HTTP_STATUS_SUCCESS, headers)
return ['']
-class KerberosWSGIExecutioner(WSGIExecutioner, HTTP_Status, KerberosSession):
+class KerberosWSGIExecutioner(WSGIExecutioner, KerberosSession):
"""Base class for xmlserver and jsonserver_kerb
"""
def _on_finalize(self):
super(KerberosWSGIExecutioner, self)._on_finalize()
- self.kerb_session_on_finalize()
def __call__(self, environ, start_response):
self.debug('KerberosWSGIExecutioner.__call__:')
user_ccache=environ.get('KRB5CCNAME')
- headers = [('Content-Type', '%s; charset=utf-8' % self.content_type)]
+ self.headers = [('Content-Type',
+ '%s; charset=utf-8' % self.content_type)]
if user_ccache is None:
@@ -664,18 +641,19 @@ class KerberosWSGIExecutioner(WSGIExecutioner, HTTP_Status, KerberosSession):
'KRB5CCNAME not defined in HTTP request environment')
return self.marshal(None, CCacheError())
+
+ logout_cookie = getattr(context, 'logout_cookie', None)
+ if logout_cookie:
+ self.headers.append(('IPASESSION', logout_cookie))
+
try:
self.create_context(ccache=user_ccache)
response = super(KerberosWSGIExecutioner, self).__call__(
environ, start_response)
- session_data = getattr(context, 'session_data', None)
- if (session_data is None and self.env.context != 'lite'):
- self.finalize_kerberos_acquisition(
- 'xmlserver', user_ccache, environ, start_response, headers)
except PublicError as e:
status = HTTP_STATUS_SUCCESS
response = status.encode('utf-8')
- start_response(status, headers)
+ start_response(status, self.headers)
return self.marshal(None, e)
finally:
destroy_context()
@@ -773,14 +751,9 @@ class jsonserver_session(jsonserver, KerberosSession):
def __init__(self, api):
super(jsonserver_session, self).__init__(api)
- name = '{0}_{1}'.format(self.__class__.__name__, id(self))
- auth_mgr = AuthManagerKerb(name)
- session_mgr = get_session_mgr()
- session_mgr.auth_mgr.register(auth_mgr.name, auth_mgr)
def _on_finalize(self):
super(jsonserver_session, self)._on_finalize()
- self.kerb_session_on_finalize()
def need_login(self, start_response):
status = '401 Unauthorized'
@@ -798,68 +771,32 @@ class jsonserver_session(jsonserver, KerberosSession):
self.debug('WSGI jsonserver_session.__call__:')
- # Load the session data
- session_mgr = get_session_mgr()
- 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 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')
+ ccache_name = environ.get('KRB5CCNAME')
# Redirect to login if no Kerberos credentials
- if ccache_data is None:
+ if ccache_name is None:
self.debug('no ccache, need login')
return self.need_login(start_response)
- ipa_ccache_name = bind_ipa_ccache(ccache_data)
-
# Redirect to login if Kerberos credentials are expired
- creds = get_credentials_if_valid(ccache_name=ipa_ccache_name)
+ creds = get_credentials_if_valid(ccache_name=ccache_name)
if not creds:
self.debug('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 = creds.lifetime + time.time()
- self.update_session_expiration(session_data, endtime)
-
- # Store the session data in the per-thread context
- setattr(context, 'session_data', session_data)
+ # Store the ccache name in the per-thread context
+ setattr(context, 'ccache_name', ccache_name)
# This may fail if a ticket from wrong realm was handled via browser
try:
- self.create_context(ccache=ipa_ccache_name)
+ self.create_context(ccache=ccache_name)
except ACIError as e:
return self.unauthorized(environ, start_response, str(e), 'denied')
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 'ccache_data' in session_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
@@ -873,13 +810,12 @@ class jsonserver_kerb(jsonserver, KerberosWSGIExecutioner):
key = '/json'
-class KerberosLogin(Backend, KerberosSession, HTTP_Status):
+class KerberosLogin(Backend, KerberosSession):
key = None
def _on_finalize(self):
super(KerberosLogin, self)._on_finalize()
self.api.Backend.wsgi_dispatch.mount(self, self.key)
- self.kerb_session_on_finalize()
def __call__(self, environ, start_response):
self.debug('WSGI KerberosLogin.__call__:')
@@ -901,7 +837,7 @@ class login_x509(KerberosLogin):
key = '/session/login_x509'
-class login_password(Backend, KerberosSession, HTTP_Status):
+class login_password(Backend, KerberosSession):
content_type = 'text/plain'
key = '/session/login_password'
@@ -909,7 +845,6 @@ class login_password(Backend, KerberosSession, HTTP_Status):
def _on_finalize(self):
super(login_password, self)._on_finalize()
self.api.Backend.wsgi_dispatch.mount(self, self.key)
- self.kerb_session_on_finalize()
def __call__(self, environ, start_response):
self.debug('WSGI login_password.__call__:')
@@ -1243,14 +1178,9 @@ class xmlserver_session(xmlserver, KerberosSession):
def __init__(self, api):
super(xmlserver_session, self).__init__(api)
- name = '{0}_{1}'.format(self.__class__.__name__, id(self))
- auth_mgr = AuthManagerKerb(name)
- session_mgr = get_session_mgr()
- 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'
@@ -1268,64 +1198,26 @@ class xmlserver_session(xmlserver, KerberosSession):
self.debug('WSGI xmlserver_session.__call__:')
- # Load the session data
- session_mgr = get_session_mgr()
- 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')
+ ccache_name = environ.get('KRB5CCNAME')
# Redirect to /ipa/xml if no Kerberos credentials
- if ccache_data is None:
+ if ccache_name 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
- creds = get_credentials_if_valid(ccache_name=ipa_ccache_name)
+ creds = get_credentials_if_valid(ccache_name=ccache_name)
if not creds:
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 = creds.lifetime + time.time()
- 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
+ setattr(context, 'ccache_name', 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 'ccache_data' in session_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