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.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
 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())
 
 
         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)
 
         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.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
 from ipsilon.providers.saml2.auth import UnknownProvider
 from ipsilon.util.user import UserSession
 import cherrypy
@@ -204,7 +203,7 @@ class LogoutRequest(ProviderPageBase):
 
         us = UserSession()
 
 
         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)
 
         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):
 
 
 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.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,
 
     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
 
 
 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.
 class SAMLSession(Log):
     """
     A SAML login session.
@@ -118,8 +101,8 @@ class SAMLSessionFactory(Log):
 
     Returns a SAMLSession object representing the new session.
     """
 
     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):
         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
     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
 
     cherrypy_config['tools.sessions.timeout'] = 60
 
-    factory = SAMLSessionFactory()
+    factory = SAMLSessionFactory('/tmp/saml2sessions.sqlite')
     factory.wipe_data()
 
     sess1 = factory.add_session('_123456', provider1, "admin", "<Login/>")
     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.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
 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.rest = None
         self.page = None
         self.idp = None
+        self.sessionfactory = None
         self.description = """
 Provides SAML 2.0 authentication infrastructure. """
 
         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',
                 ['*']),
                 '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
         )
         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)
 
             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
         bt.start()
 
     @property
@@ -344,9 +354,13 @@ Provides SAML 2.0 authentication infrastructure. """
 
     def init_idp(self):
         idp = None
 
     def init_idp(self):
         idp = None
+        self.sessionfactory = SAMLSessionFactory(
+            database_url=self.get_config_value('session database url')
+        )
         # Init IDP data
         try:
         # 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
         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()
 
         us = UserSession()
         user = us.get_user()
 
-        saml_sessions = SAMLSessionFactory()
+        saml_sessions = self.sessionfactory
         session = saml_sessions.get_next_logout()
         if session is None:
             return
         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))
                            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':
 
     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 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
         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
 # 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 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
 import ConfigParser
 import os
 import uuid
@@ -513,9 +514,12 @@ class TranStore(Store):
 
 class SAML2SessionStore(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'
         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):
         """
 
     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]
 
             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)
 
     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}"
 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"
 
 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',
         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)
 
             cmd = ['/usr/bin/createdb', '-h', addr, '-p', port, d]
             subprocess.check_call(cmd, env=env)