Only initialize the SAML IDP when actually enabled
[cascardo/ipsilon.git] / ipsilon / providers / saml2idp.py
index ef31f36..93dcbc6 100644 (file)
@@ -7,9 +7,13 @@ 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.util.data import SAML2SessionStore
 from ipsilon.tools.certs import Certificate
 from ipsilon.tools import saml2metadata as metadata
 from ipsilon.tools import files
+from ipsilon.util.http import require_content_type
+from ipsilon.util.constants import SOAP_MEDIA_TYPE, XML_MEDIA_TYPE
 from ipsilon.util.user import UserSession
 from ipsilon.util.plugin import PluginObject
 from ipsilon.util import config as pconfig
@@ -20,9 +24,54 @@ import os
 import time
 import uuid
 
+cherrypy.tools.require_content_type = cherrypy.Tool('before_request_body',
+                                                    require_content_type)
+
+
+def is_lasso_ecp_enabled():
+    # Full ECP support appeared in lasso version 2.4.2
+    return lasso.checkVersion(2, 4, 2, lasso.CHECK_VERSION_NUMERIC)
+
+
+class SSO_SOAP(AuthenticateRequest):
+
+    def __init__(self, *args, **kwargs):
+        super(SSO_SOAP, self).__init__(*args, **kwargs)
+        self.binding = metadata.SAML2_SERVICE_MAP['sso-soap'][1]
+
+    @cherrypy.tools.require_content_type(
+        required=[SOAP_MEDIA_TYPE, XML_MEDIA_TYPE])
+    @cherrypy.tools.accept(media=[SOAP_MEDIA_TYPE, XML_MEDIA_TYPE])
+    @cherrypy.tools.response_headers(
+        headers=[('Content-Type', 'SOAP_MEDIA_TYPE')])
+    def POST(self, *args, **kwargs):
+        self.debug("SSO_SOAP.POST() begin")
+
+        self.debug("SSO_SOAP transaction provider=%s id=%s" %
+                   (self.trans.provider, self.trans.transaction_id))
+
+        us = UserSession()
+        us.remote_login()
+        user = us.get_user()
+        self.debug("SSO_SOAP user=%s" % (user.name))
+
+        if not user:
+            raise cherrypy.HTTPError(403, 'No user specified for SSO_SOAP')
+
+        soap_xml_doc = cherrypy.request.rfile.read()
+        soap_xml_doc = soap_xml_doc.strip()
+        self.debug("SSO_SOAP soap_xml_doc=%s" % soap_xml_doc)
+        login = self.saml2login(soap_xml_doc)
+
+        return self.auth(login)
+
 
 class Redirect(AuthenticateRequest):
 
+    def __init__(self, *args, **kwargs):
+        super(Redirect, self).__init__(*args, **kwargs)
+        self.binding = metadata.SAML2_SERVICE_MAP['sso-redirect'][1]
+
     def GET(self, *args, **kwargs):
 
         query = cherrypy.request.query_string
@@ -33,6 +82,10 @@ class Redirect(AuthenticateRequest):
 
 class POSTAuth(AuthenticateRequest):
 
+    def __init__(self, *args, **kwargs):
+        super(POSTAuth, self).__init__(*args, **kwargs)
+        self.binding = metadata.SAML2_SERVICE_MAP['sso-post'][1]
+
     def POST(self, *args, **kwargs):
 
         request = kwargs.get(lasso.SAML2_FIELD_REQUEST)
@@ -78,7 +131,7 @@ class Continue(AuthenticateRequest):
         return self.auth(login)
 
 
-class RedirectLogout(LogoutRequest):
+class Logout(LogoutRequest):
 
     def GET(self, *args, **kwargs):
         query = cherrypy.request.query_string
@@ -98,6 +151,7 @@ class SSO(ProviderPageBase):
         self.Redirect = Redirect(*args, **kwargs)
         self.POST = POSTAuth(*args, **kwargs)
         self.Continue = Continue(*args, **kwargs)
+        self.SOAP = SSO_SOAP(*args, **kwargs)
 
 
 class SLO(ProviderPageBase):
@@ -105,7 +159,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
@@ -118,7 +172,7 @@ class Metadata(ProviderPageBase):
     def GET(self, *args, **kwargs):
 
         body = self._get_metadata()
-        cherrypy.response.headers["Content-Type"] = "text/xml"
+        cherrypy.response.headers["Content-Type"] = XML_MEDIA_TYPE
         cherrypy.response.headers["Content-Disposition"] = \
             'attachment; filename="metadata.xml"'
         return body
@@ -161,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. """
 
@@ -218,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
@@ -227,6 +286,14 @@ Provides SAML 2.0 authentication infrastructure. """
             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')
@@ -279,7 +346,6 @@ 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)
@@ -287,9 +353,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
@@ -323,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.
+
+        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()
@@ -355,6 +418,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)
 
 
@@ -368,6 +447,9 @@ class IdpMetadataGenerator(object):
                               '%s/saml2/SSO/POST' % url)
         self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-redirect'],
                               '%s/saml2/SSO/Redirect' % url)
+        if is_lasso_ecp_enabled():
+            self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-soap'],
+                                  '%s/saml2/SSO/SOAP' % url)
         self.meta.add_service(metadata.SAML2_SERVICE_MAP['logout-redirect'],
                               '%s/saml2/SLO/Redirect' % url)
         self.meta.add_allowed_name_format(
@@ -396,8 +478,10 @@ 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):
+    def configure(self, opts, changes):
         if opts['saml2'] != 'yes':
             return
 
@@ -434,7 +518,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