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
This commit is contained in:
John Dennis 2012-02-19 10:02:38 -05:00 committed by Rob Crittenden
parent 9753fd4230
commit 059a90702e
6 changed files with 143 additions and 49 deletions

View File

@ -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

View File

@ -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),

View File

@ -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

View File

@ -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):

View File

@ -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]

View File

@ -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'],