Use plugin-specific configuration, better expiration
[cascardo/ipsilon.git] / ipsilon / providers / saml2idp.py
index ef31f36..f771ef7 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.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
+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
 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
 
 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):
 
 
 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
     def GET(self, *args, **kwargs):
 
         query = cherrypy.request.query_string
@@ -33,6 +82,10 @@ class Redirect(AuthenticateRequest):
 
 class POSTAuth(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)
     def POST(self, *args, **kwargs):
 
         request = kwargs.get(lasso.SAML2_FIELD_REQUEST)
@@ -98,6 +151,7 @@ class SSO(ProviderPageBase):
         self.Redirect = Redirect(*args, **kwargs)
         self.POST = POSTAuth(*args, **kwargs)
         self.Continue = Continue(*args, **kwargs)
         self.Redirect = Redirect(*args, **kwargs)
         self.POST = POSTAuth(*args, **kwargs)
         self.Continue = Continue(*args, **kwargs)
+        self.SOAP = SSO_SOAP(*args, **kwargs)
 
 
 class SLO(ProviderPageBase):
 
 
 class SLO(ProviderPageBase):
@@ -118,7 +172,7 @@ class Metadata(ProviderPageBase):
     def GET(self, *args, **kwargs):
 
         body = self._get_metadata()
     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
         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.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. """
 
@@ -218,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
@@ -227,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')
@@ -287,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
@@ -326,27 +397,15 @@ Provides SAML 2.0 authentication infrastructure. """
         """
         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
+        session = saml_sessions.get_next_logout()
         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()
@@ -355,6 +414,21 @@ 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, "")
+        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)
 
 
@@ -368,6 +442,9 @@ class IdpMetadataGenerator(object):
                               '%s/saml2/SSO/POST' % url)
         self.meta.add_service(metadata.SAML2_SERVICE_MAP['sso-redirect'],
                               '%s/saml2/SSO/Redirect' % url)
                               '%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(
         self.meta.add_service(metadata.SAML2_SERVICE_MAP['logout-redirect'],
                               '%s/saml2/SLO/Redirect' % url)
         self.meta.add_allowed_name_format(
@@ -396,8 +473,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
 
@@ -434,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