# -*- coding: UTF-8 -*-


# Expression
# By: Frederic Peters <fpeters@entrouvert.com>
#     Emmanuel Raviart <eraviart@entrouvert.com>
#
# Copyright (C) 2004 Entr'ouvert, Frederic Peters & Emmanuel Raviart
#
# 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 2
# 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, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.


"""Liberty Alliance Module using old Python Lasso library.

This library is a temporary replacement for libertyalliance.py, which will be
removed as soon as the C Lasso library will be ready.
"""


import httplib
import os
import urllib
import urlparse

try:
    import lasso0.Protocols.NameRegistration as register
    import lasso0.Protocols.SingleLogout as logout
    import lasso0.Protocols.SingleSignOnAndFederation as sso
    import lasso0.Protocols.FederationTerminationNotification as termination
    import lasso0.Schemas.Schema as schema
    import lasso0.Schemas.SchemaDom as schemaDom
    import lasso0.Soap as soap
    import lasso0.Tools as lassoTools
    import lasso0.Constants as constants
except ImportError:
    sso = None
import libxml2
import libxslt

import expression.core.directories as directories
import expression.core.documents as documents
import expression.core.elements as elements
import expression.core.environs as environs
import expression.core.faults as faults
import expression.core.html as html
import expression.core.http as http
import expression.core.locations as locations
import expression.core.logs as logs
import expression.core.modules as modules
import expression.core.namespaces as namespaces
import expression.core.sessions as sessions
import expression.core.stations as stations
import expression.core.strings as strings
import expression.core.submissions as submissions
import identities


deleteSessionOnLogout = True
#symmetricKey = "Expression rules!" # FIXME


class EntityDescriptor(elements.Element):
    def getProviderId(self):
        nodes = self.evaluateXpath("@providerID")
        if not nodes:
            return None
        return nodes[0].content

    def getSingleSignOnProtocolProfiles(self):
        nodes = self.evaluateXpath("md:IDPDescriptor/md:SingleSignOnProtocolProfile")
        return [node.content for node in nodes]

    def getSingleSignOnServiceUrl(self):
        nodes = self.evaluateXpath("md:IDPDescriptor/md:SingleSignOnServiceURL")
        if not nodes:
            return None
        return nodes[0].content

    def getSoapEndpoint(self):
        nodes = self.evaluateXpath("md:IDPDescriptor/md:SoapEndpoint")
        if not nodes:
            return None
        return nodes[0].content

    providerId = property(getProviderId)
    singleSignOnProtocolProfiles = property(getSingleSignOnProtocolProfiles)
    singleSignOnServiceUrl = property(getSingleSignOnServiceUrl)
    soapEndpoint = property(getSoapEndpoint)


class LibertyAlliance(directories.Directory):
    def afterAuthentication(self, action = "afterAuthentication", authenticationRequest = None):
        """Liberty Alliance Identity Provider Method which is called once the user authentication
        requested by singleSignOn has been attempted.

        Handles HTTP GET.
        """

        session = environs.getVar("session")
        if session is None:
            raise "FIXME: Authentication failed. Return the appropriate Liberty Alliance Authentication Response"

        if authenticationRequest is None:
            # Build authentication request from query stored in session by singleSignOn.
            submission = submissions.FakeSubmission(
                query = session.libertyAllianceAuthenticationRequestQuery)
            keywords = {}
            for key in submission.getFieldKeys():
                keywords[key] = submission.getField(key)
            authenticationRequest = sso.buildAuthenticationRequestFromKeywords(keywords)

        user = environs.getVar("user")
        if user is None:
            raise "FIXME: Authentication failed. Return the appropriate Liberty Alliance Authentication Response"

        # Verify the service providerId.
        serviceProviderId = authenticationRequest.getProviderID()
        serviceProviderMetadata = self.getServiceProviderMetadata(serviceProviderId)
        if serviceProviderMetadata is None:
            raise "FIXME: Unknown service provider. Return the appropriate Liberty Alliance Authentication Response"

        identification = user.getIdentification(serviceProviderId)
        # No service identification found, create it.
        if identification is None:
            if user.identifications is None:
                user.node.newTextChild(None, "identifications", None)
            identifications = user.identifications
            identificationNode = identifications.node.newTextChild(
                None, "identification", None)
            identification = identifications.newElement(identificationNode)
            identification.providerId = serviceProviderId
            identification.localNameIdentifier \
                = lassoTools.generateNameIdentifier(serviceProviderId)
            user.identifications.append(identification)
            user.getDocument().save()

        # Process the NameIDPolicy.
        nameIdentifierPolicy = authenticationRequest.getNameIDPolicy()
        nameIdentifier = None
        if nameIdentifierPolicy == "none":
            # No federation, just return the peer/local name identifier.
            nameIdentifier = identification.peerNameIdentifier
            if not nameIdentifier:
                nameIdentifier = identification.localNameIdentifier
            assert nameIdentifier
        elif nameIdentifierPolicy == "onetime":
            # One time federation.
            nameIdentifier = lassoTools.generateNameIdentifier(peerProviderId)
            raise "FIXME: Store new nameIdentifier in identification?"
        elif nameIdentifierPolicy == "federated":
            # Federation. Return peer/local/new name identifier
            nameIdentifier = identification.peerNameIdentifier
            if not nameIdentifier:
                nameIdentifier = identification.localNameIdentifier
            assert nameIdentifier
        elif nameIdentifierPolicy == "any":
            # Federated else none.
            consent = authenticationRequest.getConsent()
            if consent:
                nameIdentifier = identification.peerNameIdentifier
                if not nameIdentifier:
                    nameIdentifier = identification.localNameIdentifier
                assert nameIdentifier
            else:
                raise "FIXME: No consent given by the principal"

        # Get nameIdentifiers formats.
        nameIdentifierFormat = "federated"
        idpNameIdentifierFormat = "federated"
        if nameIdentifierPolicy == "onetime":
            nameIdentifierFormat = "onetime"
            idpNameIdentifierFormat = "onetime"

        providerId = self.metadata.providerId

        # Build assertion.
        assertion = sso.buildAuthenticationAssertion(
                authnRequest = authenticationRequest,
                issuer = providerId,
                authenticationMethod = session.authenticationMethod,
                nameIdentifier = nameIdentifier,
                nameQualifier = serviceProviderId,
                nameIdentifierFormat = nameIdentifierFormat,
                idpNameIdentifier = identification.localNameIdentifier,
                idpNameQualifier = providerId,
                idpNameIdentifierFormat = idpNameIdentifierFormat)

        # Save the peerProviderId.
        session.peerProviderId = serviceProviderId

        # Answer to the service provider.
        profile = authenticationRequest.getProtocolProfile()
        if profile == "artifact":
            # Send an artifact, keep the assertion for later retrieval.
            raise "BOUM"
            assertionsProxy = getProxyForServerRole("assertions")
            artifact = assertionsProxy.addAssertion(assertion.exportToString())
            url = sso.getServiceProviderAssertionArtifactHandlerUrl(
                    serviceProviderMetadata.assertionConsumerServiceUrl, # FIXME
                    artifact,
                    authenticationRequest.getRelayState())
            return redirect(url)
        elif profile == "post":
            # Send an authentication response using a HTML form.
            authenticationResponse \
                = sso.buildAuthenticationResponseFromAuthnRequest(
                    providerId = providerId,
                    statusCode = "Success",
                    assertion = assertion,
                    authnRequest = authenticationRequest)

            authenticationResponseEmbedded \
                = authenticationResponse.exportToEncodedUrlEmbeddedMessage()

            layout = html.html(
                html.head(html.title(_("Authentication Succeeded"))),
                html.body(
                    html.form(
                        html.input(
                            name = "LARES", type = "hidden",
                            value = authenticationResponseEmbedded),
                        html.p(_("""\
You have been succesfully authenticated; click ok to go back to the service
provider.\
""")),
                        html.div(
                            html.span(
                                html.input(
                                    class_ = "button", name = "submit",
                                    type = "submit", value = _("OK")),
                                class_ = "action-buttons-bar"),
                            class_ = "buttons-bar"),
                        action = "https://service-provider:2003/liberty-alliance/processAuthenticationResponse",
                            # FIXME
                        enctype = "multipart/form-data", method = "post")))
            htmlDocument = documents.newTemporaryHtmlDocument()
            htmlDocument.append(layout)
            stylesheet = self.getDataHolder().getSiteXslt()
            self.outputHttpHtmlDocument(htmlDocument, stylesheet)
        else:
            # Unknown profile. We dont know what to do :
            raise "FIXME: Unknow protocol profile"

    def getCertificateFilePath(self):
        nodes = self.evaluateXpath("yep:certificateFile")
        if nodes:
            return os.path.normpath(os.path.join(self.getAbsolutePath(), nodes[0].content.strip()))
        else:
            return None

    def getCertificationAuthorityCertificateFilePath(self):
        nodes = self.evaluateXpath("yep:caCertificateFile")
        if nodes:
            return os.path.normpath(os.path.join(self.getAbsolutePath(), nodes[0].content.strip()))
        else:
            return None

    def getIdentityProviderMetadata(self):
        # FIXME: Temporary element name: yep:identityProviderMetadata
        nodes = self.evaluateXpath("yep:identityProvider/@src")
        if not nodes:
            return None
        location = nodes[0].content.strip()
        try:
            metadataHolder = self.walkToLocation(self.getSubPathInternUri(
                location))
        except faults.PathNotFound:
            return None
        return metadataHolder.getRootElement()

    def getMetadata(self):
        try:
            metadataHolder = self.walkToLocation(self.getSubPathInternUri("metadata.xml"))
        except faults.PathNotFound:
            return None
        return metadataHolder.getRootElement()

    def getPeerPublicKeyFilePath(self):
        nodes = self.evaluateXpath("yep:peerPublicKeyFile")
        if nodes:
            return os.path.normpath(os.path.join(self.getAbsolutePath(), nodes[0].content.strip()))
        else:
            return None

    def getPrivateKeyFilePath(self):
        nodes = self.evaluateXpath("yep:privateKeyFile")
        if nodes:
            return os.path.normpath(os.path.join(self.getAbsolutePath(), nodes[0].content.strip()))
        else:
            return None

    def getServiceProviderMetadata(self, providerId):
        try:
            metadataHolder = self.walkToLocation(self.getSubPathInternUri(
                "service-providers/%s" % strings.simplify(providerId)))
        except faults.PathNotFound:
            logs.debug("Service provider not found: %s" % providerId)
            return None
        return metadataHolder.getRootElement()

    def login(self):
        """Liberty Alliance service provider method that builds an
        authentication requests and then redirects to the identity provider
        login page.

        Handles HTTP GET.
        """

        command = environs.getVar("httpCommand")
        if command != "GET":
            raise faults.PathNotFound("")

        identityProviderMetadata = self.identityProviderMetadata
        assert identityProviderMetadata is not None

        submission = environs.getVar("submission")
        nextUrl = submission.getField("nextUrl")
        if not nextUrl:
            nextUrl = None

        # Compute the crypted relayState from nextUrl.
        if nextUrl:
            # FIXME: Encrypt relayState.
            # relayState = lassoTools.encryptRelayState(nextUrl, symmetricKey)
            relayState = nextUrl
        else:
            relayState = None
        cryptedRelayState = relayState # FIXME: To do.

        providerId = self.metadata.providerId

        ssoUrl = sso.getIdentityProviderAuthenticationUrl(
            authenticationMethods = (
                "smartcardPki",
                "softwarePki",
                "password"),
            profile = "post",
            nameIdPolicy = "federated",
            relayState = cryptedRelayState,
            serviceProviderId = providerId,
            singleSignOnServiceUrl = identityProviderMetadata.singleSignOnServiceUrl)
        return environs.getVar("httpRequestHandler").outputRedirect(ssoUrl)

    def processAuthenticationResponse(self):
        """Liberty Alliance Service Provider Method which processes an
        authentication response coming (indirectly) from identity provider.

        Handles HTTP GET & POST
        """

        submission = environs.getVar("submission")
        command = environs.getVar("httpCommand")
        if command == "GET":
            if submission.hasField("SAMLArt"):
                # Artifact.

                if keywords.has_key("RelayState"):
                    relayState = keywords["RelayState"]
                    del keywords["RelayState"]
                else:
                    relayState = None

                assertionArtifact = sso.buildAssertionArtifactFromKeywords(
                    keywords)
                request = sso.buildRequest(assertionArtifact)
                # FIXME: Don't use identityProviderLocation. Use instead
                # IdentityProviderSuccintID, which is encoded into the artifact.
                response = self.sendSoapRequest(request)
            else:
                # Error, unknown profile.
                response = sso.buildAuthenticationResponseFromKeywords(keywords)
        elif command == "POST":
            if submission.hasField("LARES"):
                authenticationResponseEmbedded = submission.getField("LARES")

                # Remove base64 url encoding.
                authenticationResponseEmbeddedDecoded \
                    = schema.decodeEncodedUrlEmbeddedMessage(
                        authenticationResponseEmbedded)

                authenticationResponseKeywords = {}
                for segment in authenticationResponseEmbeddedDecoded.split("&"):
                    keyAndValue = segment.split("=", 1)
                    if len(keyAndValue) != 2:
                        logs.info(
                            """Wrong segment "segment" in response.""" % segment)
                        continue
                    key, value = keyAndValue
                    authenticationResponseKeywords[key] = value

                response = sso.buildAuthenticationResponseFromKeywords(
                    authenticationResponseKeywords)
            else:
                # Error, unknown profile.
                response = sso.buildAuthenticationResponseFromKeywords(keywords)
        else:
            raise faults.PathNotFound("")
        
        relayState = response.getRelayState()
        statusCode = response.getStatusCode()
        if statusCode != "Success":
            message = _("Liberty Alliance authentication response"
                        " failed (reason = %s).") % statusCode
            return environs.getVar("httpRequestHandler").outputAlert(
                message, title = N_("Failure"), url = self.constructUri("/"))

        assertion = response.getAssertion()
        idpNameIdentifier = assertion.getIDPProvidedNameIdentifier()
        nameIdentifier = assertion.getNameIdentifier()

        identityProviderMetadata = self.identityProviderMetadata
        identityProviderId = identityProviderMetadata.providerId
            # FIXME? Use providerId stored in response.

        identitiesDirectoryHolder = self.walkToLocation("/identities")
        identitiesDirectory = identitiesDirectoryHolder.getRootElement()
        if nameIdentifier and nameIdentifier != idpNameIdentifier:
            user = identitiesDirectory.checkLocalNameIdentifier(
                identityProviderId, nameIdentifier)
        else:
            user = identitiesDirectory.checkPeerNameIdentifier(
                identityProviderId, idpNameIdentifier)
        if user is None:
            identityFeature = modules.getElementFeature(
                namespaces.yep.uri, "identity")
            userHolder = identityFeature.newXmlHolder(identitiesDirectoryHolder)
            user = userHolder.getRootElement()
##             user.language = environs.getVar("readLanguages")[0]
            user.node.newTextChild(None, "identifications", None)
            identifications = user.identifications
            identificationNode = identifications.node.newTextChild(
                None, "identification", None)
            identification = identifications.newElement(identificationNode)
            user.identifications.append(identification)

            identification.providerId = identityProviderId
            if nameIdentifier and nameIdentifier != idpNameIdentifier:
                identification.localNameIdentifier = nameIdentifier
            identification.peerNameIdentifier = idpNameIdentifier
            # The following line is done below.
            # user.getDocument().save()

        session = sessions.getOrCreateSession()
        session.publishToken = True
        session.peerProviderId = identityProviderId
        session.authenticationMethod = assertion.getAuthenticationMethod()
        session.userLocation = user.getUriInternPath()
        user.sessionLocation = session.getUriInternPath()
        user.getDocument().save()
        baseEnviron = environs.get(_level = "handleHttpCommand")
        baseEnviron.setVar("user", user)
        logs.debug(
            """Setting session user = "%s" at location "%s".""" % (
                user.simpleLabel, session.userLocation))

##         # Change the Name Identifier :
##         # change in local data :
##         peerProviderId = session["peerProviderId"]
##         localNameIdentifier = self.getLocalNameIdentifier(peerProviderId)
##         peerNameIdentifier = self.getPeerNameIdentifier(peerProviderId)
##         newLocalNameIdentifier = lassoTools.generateNameIdentifier(
##             peerProviderId)
##         self.setLocalNameIdentifier(peerProviderId, newLocalNameIdentifier)
##         # change to the remote IDP :
##         registerRequest = register.buildRegisterNameIdentifierRequest(
##             peerProviderId,
##             peerNameIdentifier, newLocalNameIdentifier, localNameIdentifier)
##         response = self.sendSoapRequestMessage(registerRequest)
##         # FIXME : process the response

        if relayState:
            # FIXME: Uncrypt relayState.
            # nextUrl = lassoTools.decryptRelayState(relayState, symmetricKey)
            nextUrl = relayState
        else:
            nextUrl = None
        if not nextUrl:
            nextUrl = "/"
        nextUrl = self.constructUri(nextUrl)
        return environs.getVar("httpRequestHandler").outputRedirect(nextUrl)

    def singleSignOn(self):
        """Liberty Alliance Identity Provider Method which processes an
        authentication request coming (indirectly) from a service provider.
        """

        command = environs.getVar("httpCommand")
        if command != "GET":
            raise faults.PathNotFound("")

        # Build the Authentication Request.
        submission = environs.getVar("submission")
        keywords = {}
        for key in submission.getFieldKeys():
            keywords[key] = submission.getField(key)
        authenticationRequest = sso.buildAuthenticationRequestFromKeywords(keywords)

        # Verify the providerId.
        serviceProviderId = authenticationRequest.getProviderID()
        serviceProviderMetadata = self.getServiceProviderMetadata(serviceProviderId)
        if serviceProviderMetadata is None:
            raise "FIXME: Unknown service provider. Return the appropriate Liberty Alliance Authentication Response"

        # Verify the signature of the AuthnRequest if needed.
        # authnRequestSigned = serviceProviderMetadata.authnRequestSigned
        # if authnRequestSigned:
        #     # FIXME: To do.
        #     pass

        # Process the authentication method.
        authenticationMethods = authenticationRequest.getAuthenticationMethods(
            )
        strongerMethods = ["smartcardPki", "softwarePki", "password"]
        chosenAuthenticationMethod = "smartcardPki"
        if len(authenticationMethods) > 0:
            # choose the default authentication of the IDP :
            authenticationComparison = \
                authenticationRequest.getAuthnContextComparison()
            if authenticationComparison == "exact":
                # choose the exactly requested method :
                for method in authenticationMethods:
                    if method in strongerMethods:
                        chosenAuthenticationMethod = method
                        break
            elif authenticationComparison == "minimum":
                # choose a method as strong as the one requested:
                for method in strongerMethods:
                    if method in authenticationMethods:
                        chosenAuthenticationMethod = method
                        break
            elif authenticationComparison == "better":
                # FIXME : ...
                pass
        logs.debug("Authentication method: %s" % chosenAuthenticationMethod)

        # FIXME : define a way to know if the authentication process
        # can be passive or not.

        # process isPassive and forceAuthn :
        isPassive, forceAuthentication = sso.processAuthenticationRequest(
            authenticationRequest)

        user = environs.getVar("user")
        if isPassive:
            # do not interact with the principal :
            if user is None:
                url = sso.getServiceProviderAssertionArtifactHandlerUrl(
                    serviceProviderMetadata.assertionConsumerServiceUrl, # FIXME
                    sso.buildStatus("noPassive"))
                return environs.getVar("httpRequestHandler").outputRedirect(url)
            return self.afterAuthentication(
                "singleSignOn", authenticationRequest)
        elif forceAuthentication or user is None:
            # Force user authentication even if he was already authenticated.
            # FIXME: Use chosenAuthenticationMethod to select the
            # authentication method to use.

            # Save the query. It will be used after authentication.
            session = sessions.getOrCreateSession()
            session.publishToken = True
            query = environs.getVar("httpQuery")
            session.libertyAllianceAuthenticationRequestQuery = query

            return self.walkToLocation(
                "/accounts/passwords/login", "GET",
                nextUrl = self.getActionUri("afterAuthentication"))
        else:
            # User is already authenticated and there is no need to
            # reauthenticate him.
            return self.afterAuthentication(
                "singleSignOn", authenticationRequest)

    certificateFilePath = property(getCertificateFilePath)
    certificationAuthorityCertificateFilePath = property(
        getCertificationAuthorityCertificateFilePath)
    identityProviderMetadata = property(getIdentityProviderMetadata)
    metadata = property(getMetadata)
    peerPublicKeyFilePath = property(getPeerPublicKeyFilePath)
    privateKeyFilePath = property(getPrivateKeyFilePath)
    

elements.registerElement(
    namespaces.md.uri, "EntityDescriptor", EntityDescriptor,
    "http://www.entrouvert.org/expression/schemas/liberty-metadata-v1.0.xsd")
elements.registerElement(
    namespaces.yep.uri, "libertyAlliance", LibertyAlliance,
    "http://www.entrouvert.org/expression/schemas/Directory.xsd",
    "http://www.entrouvert.org/expression/descriptions/Directory.xml")
