Add support for IdP-initiated login
[cascardo/ipsilon.git] / ipsilon / providers / saml2idp.py
index efaf67e..78e7778 100644 (file)
@@ -1,5 +1,6 @@
 # Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING
 
+from ipsilon.login.common import LoginHelper
 from ipsilon.providers.common import ProviderBase, ProviderPageBase, \
     ProviderInstaller
 from ipsilon.providers.saml2.auth import AuthenticateRequest
@@ -8,7 +9,6 @@ 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.sessions import expire_sessions
 from ipsilon.tools.certs import Certificate
 from ipsilon.tools import saml2metadata as metadata
 from ipsilon.tools import files
@@ -29,14 +29,16 @@ cherrypy.tools.require_content_type = cherrypy.Tool('before_request_body',
 
 
 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)
+    # Look for an exported symbol we know was added with ECP support
+    return 'ECP_ERROR_MISSING_AUTHN_REQUEST' in dir(lasso)
 
 
-class SSO_SOAP(AuthenticateRequest):
+class SSO_SOAP(AuthenticateRequest, LoginHelper):
 
-    def __init__(self, *args, **kwargs):
-        super(SSO_SOAP, self).__init__(*args, **kwargs)
+    def __init__(self, site, provider, *args, **kwargs):
+        super(SSO_SOAP, self).__init__(site, provider, *args, **kwargs)
+        # pylint: disable=protected-access
+        self.info = provider._root.login.info
         self.binding = metadata.SAML2_SERVICE_MAP['sso-soap'][1]
 
     @cherrypy.tools.require_content_type(
@@ -50,13 +52,11 @@ class SSO_SOAP(AuthenticateRequest):
         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:
+        username, auth_type = self.get_external_auth_info()
+        if not username:
             raise cherrypy.HTTPError(403, 'No user specified for SSO_SOAP')
+        self.debug("SSO_SOAP user=%s auth_type=%s" % (username, auth_type))
+        self.initialize_login_session(username, self.info, auth_type)
 
         soap_xml_doc = cherrypy.request.rfile.read()
         soap_xml_doc = soap_xml_doc.strip()
@@ -68,22 +68,25 @@ class SSO_SOAP(AuthenticateRequest):
 
 class Redirect(AuthenticateRequest):
 
-    def __init__(self, *args, **kwargs):
-        super(Redirect, self).__init__(*args, **kwargs)
+    def __init__(self, site, provider, *args, **kwargs):
+        super(Redirect, self).__init__(site, provider, *args, **kwargs)
         self.binding = metadata.SAML2_SERVICE_MAP['sso-redirect'][1]
 
     def GET(self, *args, **kwargs):
 
         query = cherrypy.request.query_string
 
-        login = self.saml2login(query)
+        spidentifier = kwargs.get('SPIdentifier')
+        relaystate = kwargs.get(lasso.SAML2_FIELD_RELAYSTATE)
+
+        login = self.saml2login(query, spidentifier, relaystate)
         return self.auth(login)
 
 
 class POSTAuth(AuthenticateRequest):
 
-    def __init__(self, *args, **kwargs):
-        super(POSTAuth, self).__init__(*args, **kwargs)
+    def __init__(self, site, provider, *args, **kwargs):
+        super(POSTAuth, self).__init__(site, provider, *args, **kwargs)
         self.binding = metadata.SAML2_SERVICE_MAP['sso-post'][1]
 
     def POST(self, *args, **kwargs):
@@ -113,7 +116,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']
@@ -121,17 +124,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
@@ -146,20 +149,20 @@ class RedirectLogout(LogoutRequest):
 
 class SSO(ProviderPageBase):
 
-    def __init__(self, *args, **kwargs):
-        super(SSO, self).__init__(*args, **kwargs)
-        self.Redirect = Redirect(*args, **kwargs)
-        self.POST = POSTAuth(*args, **kwargs)
-        self.Continue = Continue(*args, **kwargs)
-        self.SOAP = SSO_SOAP(*args, **kwargs)
+    def __init__(self, site, provider, *args, **kwargs):
+        super(SSO, self).__init__(site, provider)
+        self.Redirect = Redirect(site, provider, *args, **kwargs)
+        self.POST = POSTAuth(site, provider, *args, **kwargs)
+        self.Continue = Continue(site, provider, *args, **kwargs)
+        self.SOAP = SSO_SOAP(site, provider, *args, **kwargs)
 
 
 class SLO(ProviderPageBase):
 
-    def __init__(self, *args, **kwargs):
-        super(SLO, self).__init__(*args, **kwargs)
+    def __init__(self, site, provider, *args, **kwargs):
+        super(SLO, self).__init__(site, provider)
         self.debug('SLO init')
-        self.Redirect = RedirectLogout(*args, **kwargs)
+        self.Redirect = Logout(site, provider, *args, **kwargs)
 
 
 # one week
@@ -200,11 +203,11 @@ class Metadata(ProviderPageBase):
 
 class SAML2(ProviderPageBase):
 
-    def __init__(self, *args, **kwargs):
-        super(SAML2, self).__init__(*args, **kwargs)
-        self.metadata = Metadata(*args, **kwargs)
-        self.SSO = SSO(*args, **kwargs)
-        self.SLO = SLO(*args, **kwargs)
+    def __init__(self, site, provider, *args, **kwargs):
+        super(SAML2, self).__init__(site, provider)
+        self.metadata = Metadata(site, provider, *args, **kwargs)
+        self.SSO = SSO(site, provider, *args, **kwargs)
+        self.SLO = SLO(site, provider, *args, **kwargs)
 
 
 class IdpProvider(ProviderBase):
@@ -215,6 +218,7 @@ class IdpProvider(ProviderBase):
         self.rest = None
         self.page = None
         self.idp = None
+        self.sessionfactory = None
         self.description = """
 Provides SAML 2.0 authentication infrastructure. """
 
@@ -272,6 +276,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
@@ -281,8 +289,8 @@ Provides SAML 2.0 authentication infrastructure. """
             logger.addHandler(lh)
             logger.setLevel(logging.DEBUG)
 
-        bt = cherrypy.process.plugins.BackgroundTask(60, expire_sessions)
-        bt.start()
+    def get_providers(self):
+        return self.admin.providers
 
     @property
     def allow_self_registration(self):
@@ -336,19 +344,26 @@ 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')
+        )
         # 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)
@@ -364,7 +379,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
 
@@ -380,13 +395,18 @@ 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 = SAMLSessionFactory()
-        session = saml_sessions.get_next_logout()
+        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
 
@@ -404,7 +424,8 @@ Provides SAML 2.0 authentication infrastructure. """
         # 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, "")
+        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)
 
@@ -459,6 +480,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':
@@ -497,7 +520,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