pam: use a pam object method instead of pam module function
[cascardo/ipsilon.git] / ipsilon / providers / saml2 / auth.py
index ddebd8c..cc41bb8 100644 (file)
@@ -1,55 +1,57 @@
-# 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):
 
-    def __init__(self, *args, **kwargs):
-        super(AuthenticateRequest, self).__init__(*args, **kwargs)
+    def __init__(self, site, provider, *args, **kwargs):
+        super(AuthenticateRequest, self).__init__(site, provider)
         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,27 +95,69 @@ 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
 
-    def saml2login(self, request):
+    def _idp_initiated_login(self, spidentifier, relaystate):
+        """
+        Perform an Idp-initiated login
+
+        Exceptions are handled by the caller
+        """
+        login = self.cfg.idp.get_login_handler()
+
+        login.initIdpInitiatedAuthnRequest(spidentifier)
+
+        # Hardcode for now, handle Artifact later
+        login.request.protocolBinding = lasso.SAML2_METADATA_BINDING_POST
+
+        login.processAuthnRequestMsg()
+
+        if relaystate is not None:
+            login.msgRelayState = relaystate
+        else:
+            provider = ServiceProvider(self.cfg, login.remoteProviderId)
+            if provider.splink is not None:
+                login.msgRelayState = provider.splink
+            else:
+                login.msgRelayState = login.remoteProviderId
+
+        return login
 
-        if not request:
+    def saml2login(self, request, spidentifier=None, relaystate=None):
+        """
+        request: the SAML request
+        spidentifier: the provider ID for IdP-initiated login
+        relaystate: optional string to direct user to particular place on
+                    the SP after sending POST. If one is not provided then
+                    the protected site from the SP is used, otherwise it
+                    is set to the remote provider ID.
+        """
+        if not request and not spidentifier:
             raise cherrypy.HTTPError(400,
                                      'SAML request token missing or empty')
 
-        try:
-            login = self._parse_request(request)
-        except InvalidRequest, e:
-            self._debug(str(e))
-            raise cherrypy.HTTPError(400, 'Invalid SAML request token')
-        except UnknownProvider, e:
-            self._debug(str(e))
-            raise cherrypy.HTTPError(400, 'Unknown Service Provider')
-        except Exception, e:  # pylint: disable=broad-except
-            self._debug(str(e))
-            raise cherrypy.HTTPError(500)
+        if spidentifier:
+            try:
+                login = self._idp_initiated_login(spidentifier, relaystate)
+            except lasso.ServerProviderNotFoundError:
+                raise cherrypy.HTTPError(400, 'Unknown Service Provider')
+            except Exception, e:  # pylint: disable=broad-except
+                self.debug(str(e))
+                raise cherrypy.HTTPError(500)
+        else:
+            try:
+                login = self._parse_request(request)
+            except InvalidRequest, e:
+                self.debug(str(e))
+                raise cherrypy.HTTPError(400, 'Invalid SAML request token')
+            except UnknownProvider, e:
+                self.debug(str(e))
+                raise cherrypy.HTTPError(400, 'Unknown Service Provider')
+            except Exception, e:  # pylint: disable=broad-except
+                self.debug(str(e))
+                raise cherrypy.HTTPError(500)
 
         return login
 
@@ -182,37 +226,54 @@ 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
             login.assertion.subject.nameId.content = nameid
         else:
             self.trans.wipe()
+            self.error('Authentication succeeded but it was not ' +
+                       'provided by NameID %s' % nameidfmt)
             raise AuthenticationError("Unavailable Name ID type",
                                       lasso.SAML2_STATUS_CODE_AUTHN_FAILED)
 
-        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 = ()
-
-        # Check attribute policy and perform mapping and filtering
-        policy = Policy(self.cfg.default_attribute_mapping,
-                        self.cfg.default_allowed_attributes)
+            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()
         mappedattrs, _ = policy.map_attributes(userattrs)
         attributes = policy.filter_attributes(mappedattrs)
@@ -222,6 +283,17 @@ class AuthenticateRequest(ProviderPageBase):
 
         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] == '_':
@@ -246,23 +318,17 @@ 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()
+        saml_sessions = self.cfg.idp.sessionfactory
 
         lasso_session = lasso.Session()
         lasso_session.addAssertion(login.remoteProviderId, login.assertion)
+        provider = ServiceProvider(self.cfg, login.remoteProviderId)
         saml_sessions.add_session(login.assertion.id,
                                   login.remoteProviderId,
-                                  lasso_session)
-        us.save_provider_data('saml2', saml_sessions)
+                                  user.name,
+                                  lasso_session.dump(),
+                                  None,
+                                  provider.logout_mechs)
 
     def saml2error(self, login, code, message):
         status = lasso.Samlp2Status()
@@ -278,7 +344,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,
@@ -288,8 +354,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)