Use plugin-specific configuration, better expiration
authorRob Crittenden <rcritten@redhat.com>
Mon, 11 May 2015 22:14:42 +0000 (18:14 -0400)
committerPatrick Uiterwijk <puiterwijk@redhat.com>
Mon, 11 May 2015 22:39:31 +0000 (00:39 +0200)
Use a SAML2 plugin specific option to specify the database uri
for sessions.

Use a much more robust method to find sessions that need
expiration (thanks Patrick).

https://fedorahosted.org/ipsilon/ticket/90

Signed-off-by: Rob Crittenden <rcritten@redhat.com>
Reviewed-by: Patrick Uiterwijk <puiterwijk@redhat.com>
ipsilon/providers/saml2/auth.py
ipsilon/providers/saml2/logout.py
ipsilon/providers/saml2/provider.py
ipsilon/providers/saml2/sessions.py
ipsilon/providers/saml2idp.py
ipsilon/util/data.py
templates/install/ipsilon.conf
tests/helpers/common.py

index 495e5a9..c46d604 100644 (file)
@@ -5,7 +5,6 @@ from ipsilon.providers.common import AuthenticationError, InvalidRequest
 from ipsilon.providers.saml2.provider import ServiceProvider
 from ipsilon.providers.saml2.provider import InvalidProviderId
 from ipsilon.providers.saml2.provider import NameIdNotAllowed
-from ipsilon.providers.saml2.sessions import SAMLSessionFactory
 from ipsilon.tools import saml2metadata as metadata
 from ipsilon.util.policy import Policy
 from ipsilon.util.user import UserSession
@@ -275,7 +274,7 @@ class AuthenticateRequest(ProviderPageBase):
 
         self.debug('Assertion: %s' % login.assertion.dump())
 
-        saml_sessions = SAMLSessionFactory()
+        saml_sessions = self.cfg.idp.sessionfactory
 
         lasso_session = lasso.Session()
         lasso_session.addAssertion(login.remoteProviderId, login.assertion)
index d20370a..cc9b777 100644 (file)
@@ -2,7 +2,6 @@
 
 from ipsilon.providers.common import ProviderPageBase
 from ipsilon.providers.common import InvalidRequest
-from ipsilon.providers.saml2.sessions import SAMLSessionFactory
 from ipsilon.providers.saml2.auth import UnknownProvider
 from ipsilon.util.user import UserSession
 import cherrypy
@@ -204,7 +203,7 @@ class LogoutRequest(ProviderPageBase):
 
         us = UserSession()
 
-        saml_sessions = SAMLSessionFactory()
+        saml_sessions = self.cfg.idp.sessionfactory
 
         if lasso.SAML2_FIELD_REQUEST in message:
             self._handle_logout_request(us, logout, saml_sessions, message)
index c8425bb..3dea631 100644 (file)
@@ -266,12 +266,13 @@ class ServiceProviderCreator(object):
 
 
 class IdentityProvider(Log):
-    def __init__(self, config):
+    def __init__(self, config, sessionfactory):
         self.server = lasso.Server(config.idp_metadata_file,
                                    config.idp_key_file,
                                    None,
                                    config.idp_certificate_file)
         self.server.role = lasso.PROVIDER_ROLE_IDP
+        self.sessionfactory = sessionfactory
 
     def add_provider(self, sp):
         self.server.addProviderFromBuffer(lasso.PROVIDER_ROLE_SP,
index 6b3d860..1000a87 100644 (file)
@@ -11,23 +11,6 @@ LOGGING_OUT = 4
 LOGGED_OUT = 8
 
 
-def expire_sessions():
-    """
-    Find all expired sessions and remove them. This is executed as a
-    background cherrypy task.
-    """
-    ss = SAML2SessionStore()
-    data = ss.get_data()
-    now = datetime.datetime.now()
-    for idval in data:
-        r = data[idval]
-        exp = r.get('expiration_time', None)
-        if exp is not None:
-            exp = datetime.datetime.strptime(exp, '%Y-%m-%d %H:%M:%S.%f')
-            if exp < now:
-                ss.remove_session(idval)
-
-
 class SAMLSession(Log):
     """
     A SAML login session.
@@ -118,8 +101,8 @@ class SAMLSessionFactory(Log):
 
     Returns a SAMLSession object representing the new session.
     """
-    def __init__(self):
-        self._ss = SAML2SessionStore()
+    def __init__(self, database_url):
+        self._ss = SAML2SessionStore(database_url=database_url)
         self.user = None
 
     def _data_to_samlsession(self, uuidval, data):
@@ -288,10 +271,9 @@ if __name__ == '__main__':
     provider2 = "http://127.0.0.11/saml2"
 
     # temporary values to simulate cherrypy
-    cherrypy_config['saml2.sessions.db'] = '/tmp/saml2sessions.sqlite'
     cherrypy_config['tools.sessions.timeout'] = 60
 
-    factory = SAMLSessionFactory()
+    factory = SAMLSessionFactory('/tmp/saml2sessions.sqlite')
     factory.wipe_data()
 
     sess1 = factory.add_session('_123456', provider1, "admin", "<Login/>")
index efaf67e..f771ef7 100644 (file)
@@ -8,7 +8,7 @@ from ipsilon.providers.saml2.admin import Saml2AdminPage
 from ipsilon.providers.saml2.rest import Saml2RestBase
 from ipsilon.providers.saml2.provider import IdentityProvider
 from ipsilon.providers.saml2.sessions import SAMLSessionFactory
-from ipsilon.providers.saml2.sessions import expire_sessions
+from ipsilon.util.data import SAML2SessionStore
 from ipsilon.tools.certs import Certificate
 from ipsilon.tools import saml2metadata as metadata
 from ipsilon.tools import files
@@ -215,6 +215,7 @@ class IdpProvider(ProviderBase):
         self.rest = None
         self.page = None
         self.idp = None
+        self.sessionfactory = None
         self.description = """
 Provides SAML 2.0 authentication infrastructure. """
 
@@ -272,6 +273,10 @@ Provides SAML 2.0 authentication infrastructure. """
                 'default allowed attributes',
                 'Defines a list of allowed attributes, applied after mapping',
                 ['*']),
+            pconfig.String(
+                'session database url',
+                'Database URL for SAML2 sessions',
+                'saml2.sessions.db.sqlite'),
         )
         if cherrypy.config.get('debug', False):
             import logging
@@ -281,7 +286,12 @@ Provides SAML 2.0 authentication infrastructure. """
             logger.addHandler(lh)
             logger.setLevel(logging.DEBUG)
 
-        bt = cherrypy.process.plugins.BackgroundTask(60, expire_sessions)
+        store = SAML2SessionStore(
+            database_url=self.get_config_value('session database url')
+        )
+        bt = cherrypy.process.plugins.BackgroundTask(
+            60, store.remove_expired_sessions
+        )
         bt.start()
 
     @property
@@ -344,9 +354,13 @@ Provides SAML 2.0 authentication infrastructure. """
 
     def init_idp(self):
         idp = None
+        self.sessionfactory = SAMLSessionFactory(
+            database_url=self.get_config_value('session database url')
+        )
         # Init IDP data
         try:
-            idp = IdentityProvider(self)
+            idp = IdentityProvider(self,
+                                   sessionfactory=self.sessionfactory)
         except Exception, e:  # pylint: disable=broad-except
             self.debug('Failed to init SAML2 provider: %r' % e)
             return None
@@ -385,7 +399,7 @@ Provides SAML 2.0 authentication infrastructure. """
         us = UserSession()
         user = us.get_user()
 
-        saml_sessions = SAMLSessionFactory()
+        saml_sessions = self.sessionfactory
         session = saml_sessions.get_next_logout()
         if session is None:
             return
@@ -459,6 +473,8 @@ class Installer(ProviderInstaller):
                            help=('Metadata validity period in days '
                                  '(default - %d)' %
                                  METADATA_DEFAULT_VALIDITY_PERIOD))
+        group.add_argument('--saml2-session-dburl',
+                           help='session database URL')
 
     def configure(self, opts, changes):
         if opts['saml2'] != 'yes':
@@ -497,7 +513,11 @@ class Installer(ProviderInstaller):
                   'idp certificate file': cert.cert,
                   'idp key file': cert.key,
                   'idp nameid salt': uuid.uuid4().hex,
-                  'idp metadata validity': opts['saml2_metadata_validity']}
+                  'idp metadata validity': opts['saml2_metadata_validity'],
+                  'session database url': opts['saml2_session_dburl'] or
+                  opts['database_url'] % {
+                      'datadir': opts['data_dir'],
+                      'dbname': 'saml2.sessions.db'}}
         po.save_plugin_config(config)
 
         # Update global config to add login plugin
index f90519d..53a1756 100644 (file)
@@ -1,11 +1,12 @@
 # Copyright (C) 2013 Ipsilon project Contributors, for license see COPYING
 
 import cherrypy
+import datetime
 from ipsilon.util.log import Log
 from sqlalchemy import create_engine
 from sqlalchemy import MetaData, Table, Column, Text
 from sqlalchemy.pool import QueuePool, SingletonThreadPool
-from sqlalchemy.sql import select
+from sqlalchemy.sql import select, and_
 import ConfigParser
 import os
 import uuid
@@ -513,9 +514,12 @@ class TranStore(Store):
 
 class SAML2SessionStore(Store):
 
-    def __init__(self, path=None):
-        super(SAML2SessionStore, self).__init__('saml2.sessions.db')
+    def __init__(self, database_url):
+        super(SAML2SessionStore, self).__init__(database_url=database_url)
         self.table = 'sessions'
+        # pylint: disable=protected-access
+        table = SqlQuery(self._db, self.table, UNIQUE_DATA_COLUMNS)._table
+        table.create(checkfirst=True)
 
     def _get_unique_id_from_column(self, name, value):
         """
@@ -533,6 +537,16 @@ class SAML2SessionStore(Store):
             raise ValueError("Multiple entries returned")
         return data.keys()[0]
 
+    def remove_expired_sessions(self):
+        # pylint: disable=protected-access
+        table = SqlQuery(self._db, self.table, UNIQUE_DATA_COLUMNS)._table
+        sel = select([table.columns.uuid]). \
+            where(and_(table.c.name == 'expiration_time',
+                       table.c.value <= datetime.datetime.now()))
+        # pylint: disable=no-value-for-parameter
+        d = table.delete().where(table.c.uuid.in_(sel))
+        d.execute()
+
     def get_data(self, idval=None, name=None, value=None):
         return self.get_unique_data(self.table, idval, name, value)
 
index c688f0b..b57aa55 100644 (file)
@@ -10,7 +10,6 @@ base.dir = "${staticdir}"
 admin.config.db = "${admindb}"
 user.prefs.db = "${usersdb}"
 transactions.db = "${transdb}"
-saml2.sessions.db = "${samlsessionsdb}"
 
 tools.sessions.on = True
 tools.sessions.name = "${instance}_ipsilon_session_id"
index 93d0f17..cbd516b 100755 (executable)
@@ -187,7 +187,7 @@ class IpsilonTestBase(object):
         self.processes.append(p)
         p.wait()
         for d in ['adminconfig', 'users', 'transactions', 'sessions',
-                  'saml2sessions']:
+                  'saml2.sessions.db']:
             cmd = ['/usr/bin/createdb', '-h', addr, '-p', port, d]
             subprocess.check_call(cmd, env=env)