IdP-initiated logout for current user
[cascardo/ipsilon.git] / ipsilon / providers / saml2idp.py
index 256fcf9..9bc75b3 100644 (file)
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-from ipsilon.providers.common import ProviderBase, ProviderPageBase
+from ipsilon.providers.common import ProviderBase, ProviderPageBase, \
+    ProviderInstaller
 from ipsilon.providers.saml2.auth import AuthenticateRequest
 from ipsilon.providers.saml2.logout import LogoutRequest
 from ipsilon.providers.saml2.admin import Saml2AdminPage
 from ipsilon.providers.saml2.auth import AuthenticateRequest
 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.tools.certs import Certificate
 from ipsilon.tools import saml2metadata as metadata
 from ipsilon.providers.saml2.provider import IdentityProvider
 from ipsilon.tools.certs import Certificate
 from ipsilon.tools import saml2metadata as metadata
@@ -31,6 +33,7 @@ from datetime import timedelta
 import lasso
 import os
 import time
 import lasso
 import os
 import time
+import uuid
 
 
 class Redirect(AuthenticateRequest):
 
 
 class Redirect(AuthenticateRequest):
@@ -168,6 +171,7 @@ class IdpProvider(ProviderBase):
     def __init__(self, *pargs):
         super(IdpProvider, self).__init__('saml2', 'saml2', *pargs)
         self.admin = None
     def __init__(self, *pargs):
         super(IdpProvider, self).__init__('saml2', 'saml2', *pargs)
         self.admin = None
+        self.rest = None
         self.page = None
         self.idp = None
         self.description = """
         self.page = None
         self.idp = None
         self.description = """
@@ -191,6 +195,10 @@ Provides SAML 2.0 authentication infrastructure. """
                 'idp key file',
                 'The IdP Certificate Key genearated at install time.',
                 'certificate.key'),
                 'idp key file',
                 'The IdP Certificate Key genearated at install time.',
                 'certificate.key'),
+            pconfig.String(
+                'idp nameid salt',
+                'The salt used for persistent Name IDs.',
+                None),
             pconfig.Condition(
                 'allow self registration',
                 'Allow authenticated users to register applications.',
             pconfig.Condition(
                 'allow self registration',
                 'Allow authenticated users to register applications.',
@@ -199,16 +207,25 @@ Provides SAML 2.0 authentication infrastructure. """
                 'default allowed nameids',
                 'Default Allowed NameIDs for Service Providers.',
                 metadata.SAML2_NAMEID_MAP.keys(),
                 'default allowed nameids',
                 'Default Allowed NameIDs for Service Providers.',
                 metadata.SAML2_NAMEID_MAP.keys(),
-                ['persistent', 'transient', 'email', 'kerberos', 'x509']),
+                ['unspecified', 'persistent', 'transient', 'email',
+                 'kerberos', 'x509']),
             pconfig.Pick(
                 'default nameid',
                 'Default NameID used by Service Providers.',
                 metadata.SAML2_NAMEID_MAP.keys(),
             pconfig.Pick(
                 'default nameid',
                 'Default NameID used by Service Providers.',
                 metadata.SAML2_NAMEID_MAP.keys(),
-                'persistent'),
+                'unspecified'),
             pconfig.String(
                 'default email domain',
                 'Used for users missing the email property.',
                 'example.com'),
             pconfig.String(
                 'default email domain',
                 'Used for users missing the email property.',
                 'example.com'),
+            pconfig.MappingList(
+                'default attribute mapping',
+                'Defines how to map attributes before returning them to SPs',
+                [['*', '*']]),
+            pconfig.ComplexList(
+                'default allowed attributes',
+                'Defines a list of allowed attributes, applied after mapping',
+                ['*']),
         )
         if cherrypy.config.get('debug', False):
             import logging
         )
         if cherrypy.config.get('debug', False):
             import logging
@@ -241,6 +258,10 @@ Provides SAML 2.0 authentication infrastructure. """
         return os.path.join(self.idp_storage_path,
                             self.get_config_value('idp key file'))
 
         return os.path.join(self.idp_storage_path,
                             self.get_config_value('idp key file'))
 
+    @property
+    def idp_nameid_salt(self):
+        return self.get_config_value('idp nameid salt')
+
     @property
     def default_allowed_nameids(self):
         return self.get_config_value('default allowed nameids')
     @property
     def default_allowed_nameids(self):
         return self.get_config_value('default allowed nameids')
@@ -253,10 +274,19 @@ Provides SAML 2.0 authentication infrastructure. """
     def default_email_domain(self):
         return self.get_config_value('default email domain')
 
     def default_email_domain(self):
         return self.get_config_value('default email domain')
 
+    @property
+    def default_attribute_mapping(self):
+        return self.get_config_value('default attribute mapping')
+
+    @property
+    def default_allowed_attributes(self):
+        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)
     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 init_idp(self):
         return self.page
 
     def init_idp(self):
@@ -268,6 +298,8 @@ Provides SAML 2.0 authentication infrastructure. """
             self._debug('Failed to init SAML2 provider: %r' % e)
             return None
 
             self._debug('Failed to init SAML2 provider: %r' % e)
             return None
 
+        self._root.logout.add_handler(self.name, self.idp_initiated_logout)
+
         # Import all known applications
         data = self.get_data()
         for idval in data:
         # Import all known applications
         data = self.get_data()
         for idval in data:
@@ -290,6 +322,45 @@ Provides SAML 2.0 authentication infrastructure. """
             if self.admin:
                 self.admin.add_sps()
 
             if self.admin:
                 self.admin.add_sps()
 
+    def idp_initiated_logout(self):
+        """
+        Logout all SP sessions when the logout comes from the IdP.
+
+        For the current user only.
+        """
+        self._debug("IdP-initiated SAML2 logout")
+        us = UserSession()
+
+        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)
+        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.initRequest(session.provider_id)
+        try:
+            logout.buildRequestMsg()
+        except lasso.Error, e:
+            self.error('failure to build logout request msg: %s' % e)
+            raise cherrypy.HTTPRedirect(400, 'Failed to log out user: %s '
+                                        % e)
+
+        raise cherrypy.HTTPRedirect(logout.msgUrl)
+
 
 class IdpMetadataGenerator(object):
 
 
 class IdpMetadataGenerator(object):
 
@@ -303,10 +374,10 @@ class IdpMetadataGenerator(object):
                               '%s/saml2/SSO/Redirect' % url)
         self.meta.add_service(metadata.SAML2_SERVICE_MAP['logout-redirect'],
                               '%s/saml2/SLO/Redirect' % url)
                               '%s/saml2/SSO/Redirect' % url)
         self.meta.add_service(metadata.SAML2_SERVICE_MAP['logout-redirect'],
                               '%s/saml2/SLO/Redirect' % url)
-        self.meta.add_allowed_name_format(
-            lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT)
         self.meta.add_allowed_name_format(
             lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT)
         self.meta.add_allowed_name_format(
             lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT)
+        self.meta.add_allowed_name_format(
+            lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT)
         self.meta.add_allowed_name_format(
             lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL)
 
         self.meta.add_allowed_name_format(
             lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL)
 
@@ -314,11 +385,11 @@ class IdpMetadataGenerator(object):
         return self.meta.output(path)
 
 
         return self.meta.output(path)
 
 
-class Installer(object):
+class Installer(ProviderInstaller):
 
     def __init__(self, *pargs):
 
     def __init__(self, *pargs):
+        super(Installer, self).__init__()
         self.name = 'saml2'
         self.name = 'saml2'
-        self.ptype = 'provider'
         self.pargs = pargs
 
     def install_args(self, group):
         self.pargs = pargs
 
     def install_args(self, group):
@@ -359,7 +430,8 @@ class Installer(object):
         config = {'idp storage path': path,
                   'idp metadata file': 'metadata.xml',
                   'idp certificate file': cert.cert,
         config = {'idp storage path': path,
                   'idp metadata file': 'metadata.xml',
                   'idp certificate file': cert.cert,
-                  'idp key file': cert.key}
+                  'idp key file': cert.key,
+                  'idp nameid salt': uuid.uuid4().hex}
         po.save_plugin_config(config)
 
         # Update global config to add login plugin
         po.save_plugin_config(config)
 
         # Update global config to add login plugin