diff options
author | John Dennis <jdennis@redhat.com> | 2012-02-19 10:02:38 -0500 |
---|---|---|
committer | Rob Crittenden <rcritten@redhat.com> | 2012-02-27 05:55:15 -0500 |
commit | 059a90702e454b99490031bd37541304e65d35d2 (patch) | |
tree | 4b0c896c19cbac6c3f15e9fabfe1a7558b1c5f94 | |
parent | 9753fd423059e8d5725ead9a90a7cf1b9e0b9b85 (diff) | |
download | freeipa-059a90702e454b99490031bd37541304e65d35d2.tar.gz freeipa-059a90702e454b99490031bd37541304e65d35d2.tar.xz freeipa-059a90702e454b99490031bd37541304e65d35d2.zip |
Implement session activity timeout
Previously sessions expired after session_auth_duration had elapsed
commencing from the start of the session. We new support a "rolling"
expiration where the expiration is advanced by session_auth_duration
everytime the session is accessed, this is equivalent to a inactivity
timeout. The expiration is still constrained by the credential
expiration in all cases. The session expiration behavior is
configurable based on the session_auth_duration_type.
* Reduced the default session_auth_duration from 1 hour to 20 minutes.
* Replaced the sesssion write_timestamp with the access_timestamp and
update the access_timestamp whenever the session data is created,
retrieved, or written.
* Modify set_session_expiration_time to handle both an inactivity
timeout and a fixed duration.
* Introduce KerberosSession as a mixin class to share session
duration functionality with all classes manipulating session data
with Kerberos auth. This is both the non-RPC login class and the RPC
classes.
* Update make-lint to handle new classes.
* Added session_auth_duration_type config item.
* Updated default.conf.5 man page for new session_auth_duration_type item.
* Removed these unused config items: mount_xmlserver,
mount_jsonserver, webui_assets_dir
https://fedorahosted.org/freeipa/ticket/2392
-rw-r--r-- | ipa-client/man/default.conf.5 | 3 | ||||
-rw-r--r-- | ipalib/constants.py | 9 | ||||
-rw-r--r-- | ipalib/krb_utils.py | 2 | ||||
-rw-r--r-- | ipalib/session.py | 83 | ||||
-rw-r--r-- | ipaserver/rpcserver.py | 96 | ||||
-rwxr-xr-x | make-lint | 1 |
6 files changed, 144 insertions, 50 deletions
diff --git a/ipa-client/man/default.conf.5 b/ipa-client/man/default.conf.5 index 91b535ab8..ba9b1250d 100644 --- a/ipa-client/man/default.conf.5 +++ b/ipa-client/man/default.conf.5 @@ -169,6 +169,9 @@ Specifies the URI of the XML\-RPC server for a client. This is used by IPA and s .B session_auth_duration <time duration spec> Specifies the length of time authentication credentials cached in the session are valid. After the duration expires credentials will be automatically reacquired. Examples are "2 hours", "1h:30m", "10 minutes", "5min, 30sec". .TP +.B session_duration_type <inactivity_timeout|from_start> +Specifies how the expiration of a session is computed. With \fBinactivity_timeout\fR the expiration time is advanced by the value of session_auth_duration everytime the user accesses the service. With \fBfrom_start\fR the session expiration is the start of the user's session plus the value of session_auth_duration. +.TP The following define the containers for the IPA server. Containers define where in the DIT that objects can be found. The full location is the value of container + basedn. container_accounts: cn=accounts container_applications: cn=applications,cn=configs,cn=policies diff --git a/ipalib/constants.py b/ipalib/constants.py index 899c765fa..3c63739fa 100644 --- a/ipalib/constants.py +++ b/ipalib/constants.py @@ -109,15 +109,16 @@ DEFAULT_CONFIG = ( # Web Application mount points ('mount_ipa', '/ipa/'), - ('mount_xmlserver', 'xml'), - ('mount_jsonserver', 'json'), # WebUI stuff: ('webui_prod', True), - ('webui_assets_dir', None), + + # Session stuff: # Maximum time before a session expires forcing credentials to be reacquired. - ('session_auth_duration', '1h'), + ('session_auth_duration', '20 minutes'), + # How a session expiration is computed, see SessionManager.set_session_expiration_time() + ('session_duration_type', 'inactivity_timeout'), # Debugging: ('verbose', 0), diff --git a/ipalib/krb_utils.py b/ipalib/krb_utils.py index 21bca68a6..7e68bf67b 100644 --- a/ipalib/krb_utils.py +++ b/ipalib/krb_utils.py @@ -388,5 +388,5 @@ class KRB5_CCache(object): except KeyError: pass - self.debug('"%s" ccache endtime=%s', self.ccache_str(), krb5_format_time(result)) + self.debug('"%s" ccache endtime=%s (%s)', self.ccache_str(), result, krb5_format_time(result)) return result diff --git a/ipalib/session.py b/ipalib/session.py index 1f5ee379d..5c71a92b6 100644 --- a/ipalib/session.py +++ b/ipalib/session.py @@ -626,7 +626,7 @@ mod_auth_kerb. Everything else remains the same. #------------------------------------------------------------------------------- -default_max_session_lifetime = 60*60 # number of seconds +default_max_session_duration = 60*60 # number of seconds ISO8601_DATETIME_FMT = '%Y-%m-%dT%H:%M:%S' # FIXME jrd, this should be defined elsewhere def fmt_time(timestamp): @@ -888,8 +888,8 @@ class MemcacheSessionManager(SessionManager): The session ID used to identify this session data. session_start_timestamp Timestamp when this session was created. - session_write_timestamp - Timestamp when the session was last written to cache. + session_access_timestamp + Timestamp when the session was last accessed. session_expiration_timestamp Timestamp when session expires. Defaults to zero which implies no expiration. See `set_session_expiration_time()`. @@ -904,7 +904,7 @@ class MemcacheSessionManager(SessionManager): now = time.time() return {'session_id' : session_id, 'session_start_timestamp' : now, - 'session_write_timestamp' : now, + 'session_access_timestamp' : now, 'session_expiration_timestamp' : 0, } @@ -934,6 +934,12 @@ class MemcacheSessionManager(SessionManager): ''' session_key = self.session_key(session_id) session_data = self.mc.get(session_key) + + if session_data is not None: + # update the access timestamp + now = time.time() + session_data['session_access_timestamp'] = now + return session_data def get_session_id_from_http_cookie(self, cookie_header): @@ -1028,14 +1034,17 @@ class MemcacheSessionManager(SessionManager): ''' session_id = session_data['session_id'] session_key = self.session_key(session_id) + + # update the access timestamp now = time.time() - session_data['session_write_timestamp'] = now + session_data['session_access_timestamp'] = now + session_expiration_timestamp = session_data['session_expiration_timestamp'] - self.debug('store session: session_id=%s start_timestamp=%s write_timestamp=%s expiration_timestamp=%s', + self.debug('store session: 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_write_timestamp']), + fmt_time(session_data['session_access_timestamp']), fmt_time(session_data['session_expiration_timestamp'])) self.mc.set(session_key, session_data, time=session_expiration_timestamp) @@ -1072,8 +1081,8 @@ class MemcacheSessionManager(SessionManager): return result def set_session_expiration_time(self, session_data, - lifetime=default_max_session_lifetime, - max_age=None): + duration=default_max_session_duration, + max_age=None, duration_type='inactivity_timeout'): ''' memcached permits setting an expiration time on entries. The expiration time may either be Unix time (number of seconds since @@ -1088,10 +1097,24 @@ class MemcacheSessionManager(SessionManager): constraints. When a session is created it's start time is recorded in the - session data as the session_start_timestamp value. The - expiration timestamp is computed by adding the lifetime to the - session_start_timestamp. Then if the max_age is specified the - expiration is constrained to be not greater than the max_age. + session data as the session_start_timestamp value. + + There are two ways the expiration timestamp can be computed: + + from_start + A session has a fixed duration beginning with the start of + the session. The session expires when the duration + interval has elapsed relative to the start of the session. + inactivity_timeout + A session times out after a period of inactivity. The + expiration time is advanced by the value of the duration + interval everytime the session is updated. + + After the expiration is computed it may be capped at a maximum + value due to other constraints (e.g. authentication credential + expiration). If the optional max_age parameter is specified + then expiration is constrained to be not greater than the + max_age. The final computed expiration is then written into the session_data as the session_expiration_timestamp value. The @@ -1107,31 +1130,51 @@ class MemcacheSessionManager(SessionManager): :parameters: session_data Session data dict, must contain session_id key. - lifetime + duration Number of seconds cache entry should live. This is a duration value, not a timestamp. Zero implies no expiration. - - max_age + max_age Unix time value when cache entry must expire by. :returns: expiration timestamp, zero implies no expiration ''' - if lifetime == 0 and max_age is None: + if duration == 0 and max_age is None: + # No expiration expiration = 0 session_data['session_expiration_timestamp'] = expiration return expiration - session_start_timestamp = session_data['session_start_timestamp'] - expiration = session_start_timestamp + lifetime - + if duration_type == 'inactivity_timeout': + now = time.time() + session_data['session_access_timestamp'] = now + expiration = now + duration + elif duration_type == 'from_start': + session_start_timestamp = session_data['session_start_timestamp'] + expiration = session_start_timestamp + duration + else: + # Don't throw an exception, it's critical the session be + # given some expiration, instead log the error and execute + # a default action of expiring the session 5 minutes after + # it was initiated (similar to from_start but with + # hardcoded duration) + default = 60*5 + self.warning('unknown session duration_type (%s), defaulting to %s seconds from session start', + duration_type, default) + session_start_timestamp = session_data['session_start_timestamp'] + expiration = session_start_timestamp + default + + # Cap the expiration if max_age is specified if max_age is not None: expiration = min(expiration, max_age) session_data['session_expiration_timestamp'] = expiration + self.debug('set_session_expiration_time: duration_type=%s duration=%s max_age=%s expiration=%s (%s)', + duration_type, duration, max_age, expiration, fmt_time(expiration)) + return expiration def delete_session_data(self, session_id): diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py index d0bb605f5..0b8aa4088 100644 --- a/ipaserver/rpcserver.py +++ b/ipaserver/rpcserver.py @@ -32,7 +32,7 @@ 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, AuthManager, 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_duration from ipalib.backend import Backend from ipalib.krb_utils import krb5_parse_ccache, KRB5_CCache, krb_ticket_expiration_threshold from wsgiref.util import shift_path_info @@ -566,7 +566,60 @@ class AuthManagerKerb(AuthManager): self.error('AuthManager.logout.%s: session_data does not contain ccache_data', self.name) -class jsonserver_session(jsonserver): +class KerberosSession(object): + ''' + Functionally shared by all RPC handlers using both sessions and + Kerberos. This class must be implemented as a mixin class rather + than the more obvious technique of subclassing because the classes + 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, 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.set_session_expiration_time(session_data, + duration=self.session_auth_duration, + max_age=krb_expiration, + duration_type=self.api.env.session_duration_type) + +class jsonserver_session(jsonserver, KerberosSession): """ JSON RPC server protected with session auth. """ @@ -578,6 +631,10 @@ class jsonserver_session(jsonserver): auth_mgr = AuthManagerKerb(self.__class__.__name__) 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' headers = [] @@ -598,10 +655,10 @@ class jsonserver_session(jsonserver): 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', + 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_write_timestamp']), + fmt_time(session_data['session_access_timestamp']), fmt_time(session_data['session_expiration_timestamp'])) ccache_data = session_data.get('ccache_data') @@ -620,6 +677,10 @@ class jsonserver_session(jsonserver): delete_krbccache_file(krbccache_pathname) return self.need_login(start_response) + # Update the session expiration based on the Kerberos expiration + endtime = cc.endtime(self.api.env.host, self.api.env.realm) + self.update_session_expiration(session_data, endtime) + # Store the session data in the per-thread context setattr(context, 'session_data', session_data) @@ -676,7 +737,7 @@ class jsonserver_kerb(jsonserver): return response -class krblogin(Backend): +class krblogin(Backend, KerberosSession): key = '/login' def __init__(self): @@ -685,17 +746,7 @@ class krblogin(Backend): def _on_finalize(self): super(krblogin, self)._on_finalize() self.api.Backend.wsgi_dispatch.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) - + self.kerb_session_on_finalize() def __call__(self, environ, start_response): headers = [] @@ -720,17 +771,10 @@ class krblogin(Backend): # Copy the ccache file contents into the session data session_data['ccache_data'] = read_krbccache_file(ccache_location) - # Compute when the session will expire + # Set when the session will expire cc = KRB5_CCache(ccache) 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 - - # Set the session expiration time - session_mgr.set_session_expiration_time(session_data, - lifetime=self.session_auth_duration, - max_age=krb_expiration) + 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) @@ -747,3 +791,5 @@ class krblogin(Backend): start_response(status, headers) return [response] + + @@ -67,6 +67,7 @@ class IPATypeChecker(TypeChecker): 'ipalib.parameters.Enum': ['values'], 'ipalib.parameters.File': ['stdin_if_missing'], 'urlparse.SplitResult': ['netloc'], + 'ipaserver.rpcserver.KerberosSession' : ['api', 'log', 'debug', 'info', 'warning', 'error', 'critical', 'exception'], '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'], |