mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-01-27 00:26:33 -06:00
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:
parent
9753fd4230
commit
059a90702e
@ -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
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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'],
|
||||
|
Loading…
Reference in New Issue
Block a user