Flip some debug messages to error messages in SAML2 provider
[cascardo/ipsilon.git] / ipsilon / providers / saml2idp.py
index 11ba832..2b61fdf 100644 (file)
@@ -7,6 +7,7 @@ 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.sessions import SAMLSessionFactory
 from ipsilon.tools.certs import Certificate
 from ipsilon.tools import saml2metadata as metadata
 from ipsilon.tools import files
@@ -111,7 +112,7 @@ class Continue(AuthenticateRequest):
         self.debug('Continue auth for %s' % user.name)
 
         if 'saml2_request' not in transdata:
-            self.debug("Couldn't find Request dump?!")
+            self.error("Couldn't find Request dump in transaction?!")
             # TODO: Return to SP with auth failed error
             raise cherrypy.HTTPError(400)
         dump = transdata['saml2_request']
@@ -119,17 +120,17 @@ class Continue(AuthenticateRequest):
         try:
             login = self.cfg.idp.get_login_handler(dump)
         except Exception, e:  # pylint: disable=broad-except
-            self.debug('Failed to load status from dump: %r' % e)
+            self.error('Failed to load login status from dump: %r' % e)
 
         if not login:
-            self.debug("Empty Request dump?!")
+            self.error("Empty login Request dump?!")
             # TODO: Return to SP with auth failed error
             raise cherrypy.HTTPError(400)
 
         return self.auth(login)
 
 
-class RedirectLogout(LogoutRequest):
+class Logout(LogoutRequest):
 
     def GET(self, *args, **kwargs):
         query = cherrypy.request.query_string
@@ -157,7 +158,7 @@ class SLO(ProviderPageBase):
     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
@@ -213,6 +214,7 @@ class IdpProvider(ProviderBase):
         self.rest = None
         self.page = None
         self.idp = None
+        self.sessionfactory = None
         self.description = """
 Provides SAML 2.0 authentication infrastructure. """
 
@@ -270,6 +272,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
@@ -279,6 +285,9 @@ Provides SAML 2.0 authentication infrastructure. """
             logger.addHandler(lh)
             logger.setLevel(logging.DEBUG)
 
+    def get_providers(self):
+        return self.admin.providers
+
     @property
     def allow_self_registration(self):
         return self.get_config_value('allow self registration')
@@ -331,19 +340,32 @@ Provides SAML 2.0 authentication infrastructure. """
         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)
         return self.page
 
+    def used_datastores(self):
+        # pylint: disable=protected-access
+        return [self.sessionfactory._ss]
+
     def init_idp(self):
         idp = None
+        self.sessionfactory = SAMLSessionFactory(
+            database_url=self.get_config_value('session database url')
+        )
+        # Schedule cleanups
+        # pylint: disable=protected-access
+        bt = cherrypy.process.plugins.BackgroundTask(
+            60, self.sessionfactory._ss.remove_expired_sessions
+        )
+        bt.start()
         # 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)
+            self.error('Failed to init SAML2 provider: %r' % e)
             return None
 
         self._root.logout.add_handler(self.name, self.idp_initiated_logout)
@@ -359,7 +381,7 @@ Provides SAML 2.0 authentication infrastructure. """
             try:
                 idp.add_provider(sp)
             except Exception, e:  # pylint: disable=broad-except
-                self.debug('Failed to add SP %s: %r' % (sp['name'], e))
+                self.error('Failed to add SP %s: %r' % (sp['name'], e))
 
         return idp
 
@@ -375,30 +397,23 @@ Provides SAML 2.0 authentication infrastructure. """
         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()
+        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
 
-        # 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.setSessionFromDump(session.session.dump())
+        logout.setSessionFromDump(session.login_session)
         logout.initRequest(session.provider_id)
         try:
             logout.buildRequestMsg()
@@ -407,6 +422,22 @@ Provides SAML 2.0 authentication infrastructure. """
             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)
 
 
@@ -451,6 +482,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':
@@ -489,7 +522,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