Only initialize the SAML IDP when actually enabled
[cascardo/ipsilon.git] / ipsilon / providers / saml2idp.py
index 6dfb03a..93dcbc6 100644 (file)
@@ -7,6 +7,8 @@ from ipsilon.providers.saml2.logout import LogoutRequest
 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.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.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
@@ -129,7 +131,7 @@ class Continue(AuthenticateRequest):
         return self.auth(login)
 
 
         return self.auth(login)
 
 
-class RedirectLogout(LogoutRequest):
+class Logout(LogoutRequest):
 
     def GET(self, *args, **kwargs):
         query = cherrypy.request.query_string
 
     def GET(self, *args, **kwargs):
         query = cherrypy.request.query_string
@@ -157,7 +159,7 @@ class SLO(ProviderPageBase):
     def __init__(self, *args, **kwargs):
         super(SLO, self).__init__(*args, **kwargs)
         self.debug('SLO init')
     def __init__(self, *args, **kwargs):
         super(SLO, self).__init__(*args, **kwargs)
         self.debug('SLO init')
-        self.Redirect = RedirectLogout(*args, **kwargs)
+        self.Redirect = Logout(*args, **kwargs)
 
 
 # one week
 
 
 # one week
@@ -213,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. """
 
@@ -270,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
@@ -279,6 +286,14 @@ Provides SAML 2.0 authentication infrastructure. """
             logger.addHandler(lh)
             logger.setLevel(logging.DEBUG)
 
             logger.addHandler(lh)
             logger.setLevel(logging.DEBUG)
 
+        store = SAML2SessionStore(
+            database_url=self.get_config_value('session database url')
+        )
+        bt = cherrypy.process.plugins.BackgroundTask(
+            60, store.remove_expired_sessions
+        )
+        bt.start()
+
     @property
     def allow_self_registration(self):
         return self.get_config_value('allow self registration')
     @property
     def allow_self_registration(self):
         return self.get_config_value('allow self registration')
@@ -331,7 +346,6 @@ Provides SAML 2.0 authentication infrastructure. """
         return self.get_config_value('default allowed attributes')
 
     def get_tree(self, site):
         return self.get_config_value('default allowed attributes')
 
     def get_tree(self, site):
-        self.idp = self.init_idp()
         self.page = SAML2(site, self)
         self.admin = Saml2AdminPage(site, self)
         self.rest = Saml2RestBase(site, self)
         self.page = SAML2(site, self)
         self.admin = Saml2AdminPage(site, self)
         self.rest = Saml2RestBase(site, self)
@@ -339,9 +353,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
@@ -375,30 +393,23 @@ Provides SAML 2.0 authentication infrastructure. """
         Logout all SP sessions when the logout comes from the IdP.
 
         For the current user only.
         Logout all SP sessions when the logout comes from the IdP.
 
         For the current user only.
+
+        Only use HTTP-Redirect to start the logout. This is guaranteed
+        to be supported in SAML 2.
         """
         self.debug("IdP-initiated SAML2 logout")
         us = UserSession()
         """
         self.debug("IdP-initiated SAML2 logout")
         us = UserSession()
+        user = us.get_user()
 
 
-        saml_sessions = us.get_provider_data('saml2')
-        if saml_sessions is None:
-            self.debug("No SAML2 sessions to logout")
-            return
-        session = saml_sessions.get_next_logout(remove=False)
+        saml_sessions = self.sessionfactory
+        # pylint: disable=unused-variable
+        (mech, session) = saml_sessions.get_next_logout(
+            logout_mechs=[lasso.SAML2_METADATA_BINDING_REDIRECT])
         if session is None:
             return
 
         if session is None:
             return
 
-        # Add a fake session to indicate where the user should
-        # be redirected to when all SP's are logged out.
-        idpurl = self._root.instance_base_url()
-        saml_sessions.add_session("_idp_initiated_logout",
-                                  idpurl,
-                                  "")
-        init_session = saml_sessions.find_session_by_provider(idpurl)
-        init_session.set_logoutstate(idpurl, "idp_initiated_logout", None)
-        saml_sessions.start_logout(init_session)
-
         logout = self.idp.get_logout_handler()
         logout = self.idp.get_logout_handler()
-        logout.setSessionFromDump(session.session.dump())
+        logout.setSessionFromDump(session.login_session)
         logout.initRequest(session.provider_id)
         try:
             logout.buildRequestMsg()
         logout.initRequest(session.provider_id)
         try:
             logout.buildRequestMsg()
@@ -407,6 +418,22 @@ Provides SAML 2.0 authentication infrastructure. """
             raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
                                         % e)
 
             raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
                                         % e)
 
+        # Add a fake session to indicate where the user should
+        # be redirected to when all SP's are logged out.
+        idpurl = self._root.instance_base_url()
+        session_id = "_" + uuid.uuid4().hex.upper()
+        saml_sessions.add_session(session_id, idpurl, user.name, "", "",
+                                  [lasso.SAML2_METADATA_BINDING_REDIRECT])
+        init_session = saml_sessions.get_session_by_id(session_id)
+        saml_sessions.start_logout(init_session, relaystate=idpurl)
+
+        # Add the logout request id we just created to the session to be
+        # logged out so that when it responds we can find the right
+        # session.
+        session.set_logoutstate(request_id=logout.request.id)
+        saml_sessions.start_logout(session, initial=False)
+
+        self.debug('Sending initial logout request to %s' % logout.msgUrl)
         raise cherrypy.HTTPRedirect(logout.msgUrl)
 
 
         raise cherrypy.HTTPRedirect(logout.msgUrl)
 
 
@@ -451,8 +478,10 @@ 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):
+    def configure(self, opts, changes):
         if opts['saml2'] != 'yes':
             return
 
         if opts['saml2'] != 'yes':
             return
 
@@ -489,7 +518,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