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

--
John Dennis <jden...@redhat.com>

Looking to carve out IT costs?
www.redhat.com/carveoutcosts/
From e7d62b6137eb5009c411e896528ef2fdf429a721 Mon Sep 17 00:00:00 2001
From: John Dennis <jden...@redhat.com>
Date: Sun, 19 Feb 2012 10:02:38 -0500
Subject: [PATCH 63] Implement session activity timeout
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 8bit

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
---
 ipa-client/man/default.conf.5 |    3 +
 ipalib/constants.py           |    9 ++--
 ipalib/krb_utils.py           |    2 +-
 ipalib/session.py             |   83 +++++++++++++++++++++++++++---------
 ipaserver/rpcserver.py        |   96 ++++++++++++++++++++++++++++++-----------
 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 91b535a..ba9b125 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 899c765..3c63739 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 21bca68..7e68bf6 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 1f5ee37..5c71a92 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 db552dc..6f06240 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
@@ -559,7 +559,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.
     """
@@ -571,6 +624,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 = []
@@ -591,10 +648,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')
@@ -613,6 +670,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)
 
@@ -669,7 +730,7 @@ class jsonserver_kerb(jsonserver):
         return response
 
 
-class krblogin(Backend):
+class krblogin(Backend, KerberosSession):
     key = '/login'
 
     def __init__(self):
@@ -678,17 +739,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 = []
@@ -713,17 +764,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)
@@ -740,3 +784,5 @@ class krblogin(Backend):
 
         start_response(status, headers)
         return [response]
+
+
diff --git a/make-lint b/make-lint
index 94fb1ca..f76a8bf 100755
--- a/make-lint
+++ b/make-lint
@@ -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'],
-- 
1.7.7.6

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to