Implement change registration
[cascardo/ipsilon.git] / ipsilon / providers / saml2idp.py
index 9bc75b3..11ba832 100644 (file)
@@ -1,19 +1,4 @@
-# Copyright (C) 2014  Simo Sorce <simo@redhat.com>
-#
-# see file 'COPYING' for use and warranty information
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+# Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING
 
 from ipsilon.providers.common import ProviderBase, ProviderPageBase, \
     ProviderInstaller
 
 from ipsilon.providers.common import ProviderBase, ProviderPageBase, \
     ProviderInstaller
@@ -25,6 +10,8 @@ from ipsilon.providers.saml2.provider import IdentityProvider
 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
@@ -35,9 +22,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
@@ -48,6 +80,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)
@@ -68,14 +104,14 @@ class Continue(AuthenticateRequest):
         self.stage = transdata['saml2_stage']
 
         if user.is_anonymous:
         self.stage = transdata['saml2_stage']
 
         if user.is_anonymous:
-            self._debug("User is marked anonymous?!")
+            self.debug("User is marked anonymous?!")
             # TODO: Return to SP with auth failed error
             raise cherrypy.HTTPError(401)
 
             # TODO: Return to SP with auth failed error
             raise cherrypy.HTTPError(401)
 
-        self._debug('Continue auth for %s' % user.name)
+        self.debug('Continue auth for %s' % user.name)
 
         if 'saml2_request' not in transdata:
 
         if 'saml2_request' not in transdata:
-            self._debug("Couldn't find Request dump?!")
+            self.debug("Couldn't find Request dump?!")
             # TODO: Return to SP with auth failed error
             raise cherrypy.HTTPError(400)
         dump = transdata['saml2_request']
             # TODO: Return to SP with auth failed error
             raise cherrypy.HTTPError(400)
         dump = transdata['saml2_request']
@@ -83,10 +119,10 @@ class Continue(AuthenticateRequest):
         try:
             login = self.cfg.idp.get_login_handler(dump)
         except Exception, e:  # pylint: disable=broad-except
         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.debug('Failed to load status from dump: %r' % e)
 
         if not login:
 
         if not login:
-            self._debug("Empty Request dump?!")
+            self.debug("Empty Request dump?!")
             # TODO: Return to SP with auth failed error
             raise cherrypy.HTTPError(400)
 
             # TODO: Return to SP with auth failed error
             raise cherrypy.HTTPError(400)
 
@@ -113,27 +149,28 @@ 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):
 
     def __init__(self, *args, **kwargs):
         super(SLO, self).__init__(*args, **kwargs)
 
 
 class SLO(ProviderPageBase):
 
     def __init__(self, *args, **kwargs):
         super(SLO, self).__init__(*args, **kwargs)
-        self._debug('SLO init')
+        self.debug('SLO init')
         self.Redirect = RedirectLogout(*args, **kwargs)
 
 
 # one week
 METADATA_RENEW_INTERVAL = 60 * 60 * 24 * 7
         self.Redirect = RedirectLogout(*args, **kwargs)
 
 
 # one week
 METADATA_RENEW_INTERVAL = 60 * 60 * 24 * 7
-# 30 days
-METADATA_VALIDITY_PERIOD = 30
+# five years (approximately)
+METADATA_DEFAULT_VALIDITY_PERIOD = 365 * 5
 
 
 class Metadata(ProviderPageBase):
     def GET(self, *args, **kwargs):
 
         body = self._get_metadata()
 
 
 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
         cherrypy.response.headers["Content-Disposition"] = \
             'attachment; filename="metadata.xml"'
         return body
@@ -149,8 +186,10 @@ class Metadata(ProviderPageBase):
         idp_cert = Certificate()
         idp_cert.import_cert(self.cfg.idp_certificate_file,
                              self.cfg.idp_key_file)
         idp_cert = Certificate()
         idp_cert.import_cert(self.cfg.idp_certificate_file,
                              self.cfg.idp_key_file)
+
+        validity = int(self.cfg.idp_metadata_validity)
         meta = IdpMetadataGenerator(self.instance_base_url(), idp_cert,
         meta = IdpMetadataGenerator(self.instance_base_url(), idp_cert,
-                                    timedelta(METADATA_VALIDITY_PERIOD))
+                                    timedelta(validity))
         body = meta.output()
         with open(self.cfg.idp_metadata_file, 'w+') as m:
             m.write(body)
         body = meta.output()
         with open(self.cfg.idp_metadata_file, 'w+') as m:
             m.write(body)
@@ -185,15 +224,20 @@ Provides SAML 2.0 authentication infrastructure. """
                 '/var/lib/ipsilon/saml2'),
             pconfig.String(
                 'idp metadata file',
                 '/var/lib/ipsilon/saml2'),
             pconfig.String(
                 'idp metadata file',
-                'The IdP Metadata file genearated at install time.',
+                'The IdP Metadata file generated at install time.',
                 'metadata.xml'),
                 'metadata.xml'),
+            pconfig.String(
+                'idp metadata validity',
+                'The IdP Metadata validity period (in days) to use when '
+                'generating new metadata.',
+                METADATA_DEFAULT_VALIDITY_PERIOD),
             pconfig.String(
                 'idp certificate file',
             pconfig.String(
                 'idp certificate file',
-                'The IdP PEM Certificate genearated at install time.',
+                'The IdP PEM Certificate generated at install time.',
                 'certificate.pem'),
             pconfig.String(
                 'idp key file',
                 'certificate.pem'),
             pconfig.String(
                 'idp key file',
-                'The IdP Certificate Key genearated at install time.',
+                'The IdP Certificate Key generated at install time.',
                 'certificate.key'),
             pconfig.String(
                 'idp nameid salt',
                 'certificate.key'),
             pconfig.String(
                 'idp nameid salt',
@@ -248,6 +292,10 @@ Provides SAML 2.0 authentication infrastructure. """
         return os.path.join(self.idp_storage_path,
                             self.get_config_value('idp metadata file'))
 
         return os.path.join(self.idp_storage_path,
                             self.get_config_value('idp metadata file'))
 
+    @property
+    def idp_metadata_validity(self):
+        return self.get_config_value('idp metadata validity')
+
     @property
     def idp_certificate_file(self):
         return os.path.join(self.idp_storage_path,
     @property
     def idp_certificate_file(self):
         return os.path.join(self.idp_storage_path,
@@ -295,7 +343,7 @@ Provides SAML 2.0 authentication infrastructure. """
         try:
             idp = IdentityProvider(self)
         except Exception, e:  # pylint: disable=broad-except
         try:
             idp = IdentityProvider(self)
         except Exception, e:  # pylint: disable=broad-except
-            self._debug('Failed to init SAML2 provider: %r' % e)
+            self.debug('Failed to init SAML2 provider: %r' % e)
             return None
 
         self._root.logout.add_handler(self.name, self.idp_initiated_logout)
             return None
 
         self._root.logout.add_handler(self.name, self.idp_initiated_logout)
@@ -311,7 +359,7 @@ Provides SAML 2.0 authentication infrastructure. """
             try:
                 idp.add_provider(sp)
             except Exception, e:  # pylint: disable=broad-except
             try:
                 idp.add_provider(sp)
             except Exception, e:  # pylint: disable=broad-except
-                self._debug('Failed to add SP %s: %r' % (sp['name'], e))
+                self.debug('Failed to add SP %s: %r' % (sp['name'], e))
 
         return idp
 
 
         return idp
 
@@ -328,12 +376,12 @@ Provides SAML 2.0 authentication infrastructure. """
 
         For the current user only.
         """
 
         For the current user only.
         """
-        self._debug("IdP-initiated SAML2 logout")
+        self.debug("IdP-initiated SAML2 logout")
         us = UserSession()
 
         saml_sessions = us.get_provider_data('saml2')
         if saml_sessions is None:
         us = UserSession()
 
         saml_sessions = us.get_provider_data('saml2')
         if saml_sessions is None:
-            self._debug("No SAML2 sessions to logout")
+            self.debug("No SAML2 sessions to logout")
             return
         session = saml_sessions.get_next_logout(remove=False)
         if session is None:
             return
         session = saml_sessions.get_next_logout(remove=False)
         if session is None:
@@ -372,6 +420,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(
@@ -395,8 +446,13 @@ class Installer(ProviderInstaller):
     def install_args(self, group):
         group.add_argument('--saml2', choices=['yes', 'no'], default='yes',
                            help='Configure SAML2 Provider')
     def install_args(self, group):
         group.add_argument('--saml2', choices=['yes', 'no'], default='yes',
                            help='Configure SAML2 Provider')
+        group.add_argument('--saml2-metadata-validity',
+                           default=METADATA_DEFAULT_VALIDITY_PERIOD,
+                           help=('Metadata validity period in days '
+                                 '(default - %d)' %
+                                 METADATA_DEFAULT_VALIDITY_PERIOD))
 
 
-    def configure(self, opts):
+    def configure(self, opts, changes):
         if opts['saml2'] != 'yes':
             return
 
         if opts['saml2'] != 'yes':
             return
 
@@ -414,9 +470,10 @@ class Installer(ProviderInstaller):
         if opts['secure'].lower() == 'no':
             proto = 'http'
         url = '%s://%s/%s' % (proto, opts['hostname'], opts['instance'])
         if opts['secure'].lower() == 'no':
             proto = 'http'
         url = '%s://%s/%s' % (proto, opts['hostname'], opts['instance'])
+        validity = int(opts['saml2_metadata_validity'])
         meta = IdpMetadataGenerator(url, cert,
         meta = IdpMetadataGenerator(url, cert,
-                                    timedelta(METADATA_VALIDITY_PERIOD))
-        if 'krb' in opts and opts['krb'] == 'yes':
+                                    timedelta(validity))
+        if 'gssapi' in opts and opts['gssapi'] == 'yes':
             meta.meta.add_allowed_name_format(
                 lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS)
 
             meta.meta.add_allowed_name_format(
                 lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS)
 
@@ -431,7 +488,8 @@ class Installer(ProviderInstaller):
                   'idp metadata file': 'metadata.xml',
                   'idp certificate file': cert.cert,
                   'idp key file': cert.key,
                   'idp metadata file': 'metadata.xml',
                   'idp certificate file': cert.cert,
                   'idp key file': cert.key,
-                  'idp nameid salt': uuid.uuid4().hex}
+                  'idp nameid salt': uuid.uuid4().hex,
+                  'idp metadata validity': opts['saml2_metadata_validity']}
         po.save_plugin_config(config)
 
         # Update global config to add login plugin
         po.save_plugin_config(config)
 
         # Update global config to add login plugin