Implement ECP in Ipsilon
[cascardo/ipsilon.git] / ipsilon / providers / saml2 / auth.py
old mode 100755 (executable)
new mode 100644 (file)
index 87f4ac8..611c9bf
@@ -1,39 +1,27 @@
-#!/usr/bin/python
-#
-# 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 ProviderPageBase, ProviderException
 from ipsilon.providers.common import AuthenticationError, InvalidRequest
 from ipsilon.providers.saml2.provider import ServiceProvider
 from ipsilon.providers.saml2.provider import InvalidProviderId
 from ipsilon.providers.saml2.provider import NameIdNotAllowed
+from ipsilon.providers.saml2.sessions import SAMLSessionsContainer
+from ipsilon.tools import saml2metadata as metadata
+from ipsilon.util.policy import Policy
 from ipsilon.util.user import UserSession
 from ipsilon.util.trans import Transaction
 import cherrypy
 import datetime
 import lasso
+import uuid
+import hashlib
 
 
 class UnknownProvider(ProviderException):
 
     def __init__(self, message):
         super(UnknownProvider, self).__init__(message)
-        self._debug(message)
+        self.debug(message)
 
 
 class AuthenticateRequest(ProviderPageBase):
@@ -42,14 +30,29 @@ class AuthenticateRequest(ProviderPageBase):
         super(AuthenticateRequest, self).__init__(*args, **kwargs)
         self.stage = 'init'
         self.trans = None
+        self.binding = None
 
     def _preop(self, *args, **kwargs):
         try:
             # generate a new id or get current one
             self.trans = Transaction('saml2', **kwargs)
-            if self.trans.cookie.value != self.trans.provider:
-                self.debug('Invalid transaction, %s != %s' % (
-                           self.trans.cookie.value, self.trans.provider))
+
+            self.debug('self.binding=%s, transdata=%s' %
+                       (self.binding, self.trans.retrieve()))
+            if self.binding is None:
+                # SAML binding is unknown, try to get it from transaction
+                transdata = self.trans.retrieve()
+                self.binding = transdata.get('saml2_binding')
+            else:
+                # SAML binding known, store in transaction
+                data = {'saml2_binding': self.binding}
+                self.trans.store(data)
+
+            # Only check for cookie for those bindings which use one
+            if self.binding not in (metadata.SAML2_SERVICE_MAP['sso-soap'][1]):
+                if self.trans.cookie.value != self.trans.provider:
+                    self.debug('Invalid transaction, %s != %s' % (
+                        self.trans.cookie.value, self.trans.provider))
         except Exception, e:  # pylint: disable=broad-except
             self.debug('Transaction initialization failed: %s' % repr(e))
             raise cherrypy.HTTPError(400, 'Invalid transaction id')
@@ -93,7 +96,7 @@ class AuthenticateRequest(ProviderPageBase):
                                                  e, message)
             raise UnknownProvider(msg)
 
-        self._debug('SP %s requested authentication' % login.remoteProviderId)
+        self.debug('SP %s requested authentication' % login.remoteProviderId)
 
         return login
 
@@ -106,13 +109,13 @@ class AuthenticateRequest(ProviderPageBase):
         try:
             login = self._parse_request(request)
         except InvalidRequest, e:
-            self._debug(str(e))
+            self.debug(str(e))
             raise cherrypy.HTTPError(400, 'Invalid SAML request token')
         except UnknownProvider, e:
-            self._debug(str(e))
+            self.debug(str(e))
             raise cherrypy.HTTPError(400, 'Unknown Service Provider')
         except Exception, e:  # pylint: disable=broad-except
-            self._debug(str(e))
+            self.debug(str(e))
             raise cherrypy.HTTPError(500)
 
         return login
@@ -127,7 +130,8 @@ class AuthenticateRequest(ProviderPageBase):
                     self.basepath, self.trans.get_GET_arg())
                 data = {'saml2_stage': 'auth',
                         'saml2_request': login.dump(),
-                        'login_return': returl}
+                        'login_return': returl,
+                        'login_target': login.remoteProviderId}
                 self.trans.store(data)
                 redirect = '%s/login?%s' % (self.basepath,
                                             self.trans.get_GET_arg())
@@ -181,17 +185,27 @@ class AuthenticateRequest(ProviderPageBase):
 
         nameid = None
         if nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_PERSISTENT:
-            # TODO map to something else ?
-            nameid = provider.normalize_username(user.name)
+            idpsalt = self.cfg.idp_nameid_salt
+            if idpsalt is None:
+                raise AuthenticationError(
+                    "idp nameid salt is not set in configuration"
+                )
+            value = hashlib.sha512()
+            value.update(idpsalt)
+            value.update(login.remoteProviderId)
+            value.update(user.name)
+            nameid = '_' + value.hexdigest()
         elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_TRANSIENT:
-            # TODO map to something else ?
-            nameid = provider.normalize_username(user.name)
+            nameid = '_' + uuid.uuid4().hex
         elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_KERBEROS:
-            nameid = us.get_data('user', 'krb_principal_name')
+            userattrs = us.get_user_attrs()
+            nameid = userattrs.get('gssapi_principal_name')
         elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_EMAIL:
             nameid = us.get_user().email
             if not nameid:
                 nameid = '%s@%s' % (user.name, self.cfg.default_email_domain)
+        elif nameidfmt == lasso.SAML2_NAME_IDENTIFIER_FORMAT_UNSPECIFIED:
+            nameid = provider.normalize_username(user.name)
 
         if nameid:
             login.assertion.subject.nameId.format = nameidfmt
@@ -201,29 +215,50 @@ class AuthenticateRequest(ProviderPageBase):
             raise AuthenticationError("Unavailable Name ID type",
                                       lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
 
-        # TODO: filter user attributes as policy requires from 'usersession'
-        if not login.assertion.attributeStatement:
-            attrstat = lasso.Saml2AttributeStatement()
-            login.assertion.attributeStatement = [attrstat]
+        # Check attribute policy and perform mapping and filtering.
+        # If the SP has its own mapping or filtering policy use that
+        # instead of the global policy.
+        if (provider.attribute_mappings is not None and
+                len(provider.attribute_mappings) > 0):
+            attribute_mappings = provider.attribute_mappings
         else:
-            attrstat = login.assertion.attributeStatement[0]
-        if not attrstat.attribute:
-            attrstat.attribute = ()
-
-        attributes = dict()
+            attribute_mappings = self.cfg.default_attribute_mapping
+        if (provider.allowed_attributes is not None and
+                len(provider.allowed_attributes) > 0):
+            allowed_attributes = provider.allowed_attributes
+        else:
+            allowed_attributes = self.cfg.default_allowed_attributes
+        self.debug("Allowed attrs: %s" % allowed_attributes)
+        self.debug("Mapping: %s" % attribute_mappings)
+        policy = Policy(attribute_mappings, allowed_attributes)
         userattrs = us.get_user_attrs()
-        for key, value in userattrs.get('userdata', {}).iteritems():
-            if type(value) is str:
-                attributes[key] = value
-        if 'groups' in userattrs:
-            attributes['group'] = userattrs['groups']
-        for _, info in userattrs.get('extras', {}).iteritems():
-            for key, value in info.items():
-                attributes[key] = value
+        mappedattrs, _ = policy.map_attributes(userattrs)
+        attributes = policy.filter_attributes(mappedattrs)
+
+        if '_groups' in attributes and 'groups' not in attributes:
+            attributes['groups'] = attributes['_groups']
+
+        self.debug("%s's attributes: %s" % (user.name, attributes))
+
+        # The saml-core-2.0-os specification section 2.7.3 requires
+        # the AttributeStatement element to be non-empty.
+        if attributes:
+            if not login.assertion.attributeStatement:
+                attrstat = lasso.Saml2AttributeStatement()
+                login.assertion.attributeStatement = [attrstat]
+            else:
+                attrstat = login.assertion.attributeStatement[0]
+            if not attrstat.attribute:
+                attrstat.attribute = ()
 
         for key in attributes:
+            # skip internal info
+            if key[0] == '_':
+                continue
             values = attributes[key]
-            if type(values) is not list:
+            if isinstance(values, dict):
+                continue
+            if not isinstance(values, list):
                 values = [values]
             for value in values:
                 attr = lasso.Saml2Attribute()
@@ -240,6 +275,24 @@ class AuthenticateRequest(ProviderPageBase):
 
         self.debug('Assertion: %s' % login.assertion.dump())
 
+        saml_sessions = us.get_provider_data('saml2')
+        if saml_sessions is None:
+            saml_sessions = SAMLSessionsContainer()
+
+        session = saml_sessions.find_session_by_provider(
+            login.remoteProviderId)
+        if session:
+            # TODO: something...
+            self.debug('Login session for this user already exists!?')
+            session.dump()
+
+        lasso_session = lasso.Session()
+        lasso_session.addAssertion(login.remoteProviderId, login.assertion)
+        saml_sessions.add_session(login.assertion.id,
+                                  login.remoteProviderId,
+                                  lasso_session)
+        us.save_provider_data('saml2', saml_sessions)
+
     def saml2error(self, login, code, message):
         status = lasso.Samlp2Status()
         status.statusCode = lasso.Samlp2StatusCode()
@@ -254,7 +307,7 @@ class AuthenticateRequest(ProviderPageBase):
             raise cherrypy.HTTPError(501)
         elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_POST:
             login.buildAuthnResponseMsg()
-            self._debug('POSTing back to SP [%s]' % (login.msgUrl))
+            self.debug('POSTing back to SP [%s]' % (login.msgUrl))
             context = {
                 "title": 'Redirecting back to the web application',
                 "action": login.msgUrl,
@@ -264,8 +317,12 @@ class AuthenticateRequest(ProviderPageBase):
                 ],
                 "submit": 'Return to application',
             }
-            # pylint: disable=star-args
             return self._template('saml2/post_response.html', **context)
 
+        elif login.protocolProfile == lasso.LOGIN_PROTOCOL_PROFILE_BRWS_LECP:
+            login.buildResponseMsg()
+            self.debug("Returning ECP: %s" % login.msgBody)
+            return login.msgBody
+
         else:
             raise cherrypy.HTTPError(500)