# -*- 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"""


import errno
import httplib
import os
import urllib
import urlparse

import lasso
import libxml2

from OpenSSL import SSL

import expression.core.directories as directories
import expression.core.dataholders as dataholders
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.filesystems as filesystems
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.things as things
import identities
import libertyallianceresponses


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


class EntityDescriptor(elements.Element):
    def getAssertionConsumerServiceUrl(self, id):
        if id:
            nodes = self.evaluateXpath("md:SPDescriptor/md:AssertionConsumerServiceURL[@id = '%s']"
                                       % id.replace("'", "&apos;"))
        else:
            nodes = None
        if not nodes:
            nodes = self.evaluateXpath(
                "md:SPDescriptor/md:AssertionConsumerServiceURL[@isDefault = 'true']")
            if not nodes:
                return None
        return nodes[0].content
        
    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(things.Thing):
    _lassoServerDump = None

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

        Handles HTTP GET.
        """

        command = environs.getVar("httpCommand")
        if command != "GET":
            raise faults.PathNotFound("")
        success = True
        session = environs.getVar("session")
        if session is None:
            success = False
        if success:
            if authnResponse is None:
                authnResponse = lasso.AuthnResponse.newFromDump(
                    session.libertyAllianceAuthenticationResponse)
        user = environs.getVar("user")
        if user is None:
            success = False
        if success:
            authenticationRequest = authnResponse.request
            serviceProviderId = authenticationRequest.get_child(
                "ProviderID").get_content()
            serviceProviderMetadata = self.getServiceProviderMetadata(
                serviceProviderId)
            if serviceProviderMetadata is None:
                success = False
        authnResponse.process_authentication_result(success)
        if success:
            assertion = lasso.Assertion(
                "issuer",
                authnResponse.get_attr_value("InResponseTo"))

            authenticationMethod = session.getAuthenticationMethod()
            if authenticationMethod == "password":
                lassoAuthenticationMethod = lasso.samlAuthenticationMethodPassword
            elif lassoAuthenticationMethod == "certificate":
                lassoAuthenticationMethod = lasso.samlAuthenticationMethodSoftwarePki
            elif lassoAuthenticationMethod == "unspecified":
                lassoAuthenticationMethod = lasso.samlAuthenticationMethodUnspecified
            else:
                raise Exception("Unknown authentication method %s" % authenticationMethod)

            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 \
                    = 'CDSICDSC7SD89CDSDCDCDCDISCDUIDSI344343'#lassoTools.generateNameIdentifier(serviceProviderId)
                user.identifications.append(identificationNode)
                user.getDocument().save()

            # Process the NameIDPolicy.
            # Remove the next lines.
            nameIdentifierPolicy = "federated"
            nameIdentifier = identification.peerNameIdentifier
            if not nameIdentifier:
                nameIdentifier = identification.localNameIdentifier
            assert nameIdentifier

##             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"

            authenticationStatement = lasso.AuthenticationStatement(
                lassoAuthenticationMethod,
                "2005-05-03T16:12:00Z", # FIXME: reauthenticateOnOrAfter
                nameIdentifier = nameIdentifier,
                nameQualifier = serviceProviderId,
                format = nameIdentifierFormat,
                idp_nameIdentifier = identification.localNameIdentifier,
                idp_nameQualifier = self.getMetadata().providerId,
                idp_format = idpNameIdentifierFormat)
            assertion.add_authenticationStatement(authenticationStatement)
            assertion.set_signature(1, self.privateKeyFilePath, self.certificateFilePath)
            authnResponse.add_assertion(assertion)

        protocolProfileLassoNode = authenticationRequest.get_child(
            "ProtocolProfile")
        if protocolProfileLassoNode is None:
            protocolProfile = "artifact"
        else:
            protocolProfile = protocolProfileLassoNode.get_content()

        # FIXME: To remove.
        if protocolProfile != lasso.libProtocolProfilePost:
            logs.debug(
                "Using POST protocol profile instead of %s." % protocolProfile)
            protocolProfile = "post"

##         if protocolProfile == lasso.libProtocolProfileArtifact:
##             # 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)
        if protocolProfile == lasso.libProtocolProfilePost:
            import base64
            lares = authnResponse.export_to_base64()

            # Remove this line and uncomment the next ones.
            assertionConsumerServiceId = None
##             assertionConsumerServiceIdLassoNode \
##                 = authenticationRequest.get_child("AssertionConsumerServiceID")
##             if assertionConsumerServiceIdLassoNode is None:
##                 assertionConsumerServiceId = None
##             else:
##                 assertionConsumerServiceId \
##                     = assertionConsumerServiceIdLassoNode.get_content()
            actionUrl = serviceProviderMetadata.getAssertionConsumerServiceUrl(
                assertionConsumerServiceId)
            layout = html.html(
                html.head(html.title(_("Authentication Succeeded"))),
                html.body(
                    html.form(
                        html.input(
                            name = "LARES", type = "hidden", value = lares),
                        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 = actionUrl,
                        enctype = "multipart/form-data", method = "post")))
            htmlDocument = documents.newTemporaryHtmlDocument()
            htmlDocument.append(layout)
            stylesheet = self.getDataHolder().getSiteXslt()
            self.outputHttpHtmlDocument(htmlDocument, stylesheet)
        else:
            # Unknown protocol profile. We dont know what to do :
            raise "FIXME: Unknow protocol profile"

    def assertionConsumer(self):
        """Liberty Alliance service provider method which processes an authentication response
        coming (indirectly) from identity provider.

        Handles HTTP GET & POST.
        """
        if self.assertionConsumerProcess():
            return self.walkToLocation("/", "GET")
        raise Exception("assertionConsumerProcess failed")

    def assertionConsumerProcess(self):
        command = environs.getVar("httpCommand")
        if command not in ("GET", "POST"):
            raise faults.PathNotFound("")

        lassoServerDump = self.getLassoServerDump()
        lassoServer = lasso.Server.newFromDump(lassoServerDump)
        lassoLogin = lasso.Login(lassoServer)
        submission = environs.getVar("submission")
        if command == "GET":
            responseMsg = environs.getVar("httpQuery")
            method = lasso.httpMethodRedirect
        else: # command == "POST"
            responseMsg = submission.getField("LAREQ")
            method = lasso.httpMethodPost
        relayState = submission.getField("RelayState", default = None)
        try:
            lassoLogin.initRequest(responseMsg, method)
        except:
            raise Exception("Lasso login error")
        try:
            lassoLogin.buildRequestMsg()
        except:
            raise Exception("Lasso login error")
        logs.debug("idpSoapEndpoint = %s; soapRequestMsg = %s" % (
            lassoLogin.msgUrl, lassoLogin.msgBody))
        print "hello6"
        soapResponseMsg = self.callSoap(lassoLogin.msgUrl, lassoLogin.msgBody)
        logs.debug("soapResponseMsg = %s" % soapResponseMsg)
        try:
            lassoLogin.processResponseMsg(soapResponseMsg)
        except:
            raise Exception("Lasso login error")
        nameIdentifier = lassoLogin.nameIdentifier

        # Retrieve user, using name identifier or else use the current user.
        user = environs.getVar("user")
        userSymlinksAbsolutePath = self.getUsersDirectoryAbsolutePath()
        userSymlinkName = strings.simplify(nameIdentifier) + ".xml"
        userSymlinkAbsolutePath = os.path.join(userSymlinksAbsolutePath, userSymlinkName)
        try:
            userPath = os.readlink(userSymlinkAbsolutePath)
        except OSError, error:
            if error.errno not in (errno.ENOENT, errno.EACCES):
                raise
            userAbsolutePath = None
        else:
            userAbsolutePath = os.path.join(userSymlinksAbsolutePath, userPath)
            if user is None or user.getAbsolutePath() != userAbsolutePath:
                try:
                    userHolder = dataholders.DataHolder(
                        pathFragment = userAbsolutePath, mimeType = "text/xml", isRootElder = True,
                        containedFileSystem = filesystems.PartialFileSystem(userAbsolutePath))
                except IOError, error:
                    if error.errno == errno.ENOENT:
                        logs.debug("""User at path "%s" doesn't exist.""" % userAbsolutePath)
                    else:
                        raise
                else:
                    # Some times, users are stored in independant files; some times they are
                    # embedded inside accounts.
                    accountOrUser = userHolder.getRootElement()
                    user = accountOrUser.getUser()
        if user is not None:
            lassoIdentityDump = user.getLassoIdentityDump()
            if lassoIdentityDump is not None:
                lassoLogin.setIdentityFromDump(lassoIdentityDump)

        # Retrieve session, using name identifier or else use the current session.
        session = environs.getVar("session")
        sessionSymlinksAbsolutePath = self.getSessionsDirectoryAbsolutePath()
        sessionSymlinkName = strings.simplify(nameIdentifier) + ".xml"
        sessionSymlinkAbsolutePath = os.path.join(sessionSymlinksAbsolutePath, sessionSymlinkName)
        try:
            sessionPath = os.readlink(sessionSymlinkAbsolutePath)
        except OSError, error:
            if error.errno not in (errno.ENOENT, errno.EACCES):
                raise
            sessionAbsolutePath = None
        else:
            sessionAbsolutePath = os.path.join(sessionSymlinksAbsolutePath, sessionPath)
            if session is None or session.getAbsolutePath() != sessionAbsolutePath:
                try:
                    sessionHolder = dataholders.DataHolder(
                        pathFragment = sessionAbsolutePath, mimeType = "text/xml",
                        isRootElder = True,
                        containedFileSystem = filesystems.PartialFileSystem(sessionAbsolutePath))
                except IOError, error:
                    if error.errno == errno.ENOENT:
                        logs.debug("""Session at path "%s" doesn't exist.""" % sessionAbsolutePath)
                    else:
                        raise
                else:
                    foundSession = sessionHolder.getRootElement()
                    if foundSession.validate():
                        session = foundSession
        if session is not None:
            lassoSessionDump = session.getLassoSessionDump()
            if lassoSessionDump is not None:
                lassoLogin.setSessionFromDump(lassoSessionDump)
        
        try:
            lassoLogin.acceptSso()
        except:
            raise Exception("Lasso login error")

        # User is now authenticated => Create user, session, cookie...
        if user is None:
            # FIXME: Call user creation or retrieval wizard.
            # Create user.
            usersPath = self.getConfigAbsolutePath("yep:usersPath")
            usersHolder = directories.DirectoryHolder(
                pathFragment = usersPath, previous = environs.getVar("rootStation"),
                isRootElder = True,
                containedFileSystem = filesystems.PartialFileSystem(usersPath))
            userFeature = modules.getElementFeature(namespaces.yep.uri, "user")
            userHolder = userFeature.newXmlHolder(usersHolder)
            user = userHolder.getRootElement()
            # FIXME: Initialize user attributes.
        if session is None:
            session = sessions.createSession()
        # Save the new or updated Lasso identity into user.
        if lassoLogin.isIdentityDirty:
            user.setLassoIdentityDump(lassoLogin.identity.dump())
        user.setSessionToken(session.getToken())
        user.getDocument().save()
        if user != environs.getVar("user"):
            user.setEnviron()
            logs.debug('Setting user = "%s" at "%s".' % (
                user.getSimpleLabel(), user.getAbsolutePath()))
        # Create a symbolic link to user, to allow to retrieve it using name identifier.
        if userAbsolutePath != user.getAbsolutePath():
            if not os.access(userSymlinksAbsolutePath, os.F_OK):
                os.makedirs(userSymlinksAbsolutePath)
            # os.access(userSymlinkAbsolutePath, os.F_OK) returns False for broken
            # symlinks, so we can't use it to test existence of symlink.
            try:
                os.remove(userSymlinkAbsolutePath)
            except OSError, error:
                if error.errno != errno.ENOENT:
                    raise
            # FIXME: Use a relative path instead of an absolute one for symbolic link.
            os.symlink(user.getAbsolutePath(), userSymlinkAbsolutePath)
        # Save the new or updated Lasso session into user.
        session.publishToken = True
        if lassoLogin.isSessionDirty:
            session.setLassoSessionDump(lassoLogin.session.dump())
        session.setAccountAbsolutePath(user.getAbsolutePath())
        if session != environs.getVar("session"):
            session.setEnviron()
            logs.debug('Setting session at "%s".' % session.getAbsolutePath())
        # Create a symbolic link to session, to allow to retrieve it using name identifier.
        if sessionAbsolutePath != session.getAbsolutePath():
            if not os.access(sessionSymlinksAbsolutePath, os.F_OK):
                os.makedirs(sessionSymlinksAbsolutePath)
            # os.access(sessionSymlinkAbsolutePath, os.F_OK) returns False for broken
            # symlinks, so we can't use it to test existence of symlink.
            try:
                os.remove(sessionSymlinkAbsolutePath)
            except OSError, error:
                if error.errno != errno.ENOENT:
                    raise
            # FIXME: Use a relative path instead of an absolute one for symbolic link.
            os.symlink(session.getAbsolutePath(), sessionSymlinkAbsolutePath)

        return True


    def callSoap(self, url, soapRequestMessage):
        parsedUrl =  urlparse.urlparse(url)
        addressingScheme, hostName, path = parsedUrl[:3]
        if addressingScheme == "https":
            privateKeyAbsolutePath = self.getConfigAbsolutePath("yep:sslCertificateKeyFile")
            certificateAbsolutePath = self.getConfigAbsolutePath("yep:sslCertificateFile")
            peerCaCertificateAbsolutePath = self.getConfigAbsolutePath(
                "yep:sslCACertificateFile", None)
            if self.getConfigBoolean("yep:libertyAllianceNoCert", default = False):
                connection = http.HttpsConnection(hostName)
            else:
                connection = http.HttpsConnection(
                    hostName, None, privateKeyAbsolutePath, certificateAbsolutePath,
                    peerCaCertificateAbsolutePath)
        else:
            connection = httplib.HTTPConnection(hostName)
        try:
            connection.request("POST", path, soapRequestMessage, {"Content-Type": "text/xml"})
        except SSL.Error, e:
            if e.args and e.args[0] and e.args[0][0] and e.args[0][0][0] == 'SSL routines':
                logs.debug(repr(e))
                return environs.getVar("httpRequestHandler").outputAlert(e.args[0][0][2])
            raise
        soapResponseFile = connection.getresponse()
        try:
            soapResponseMsg = soapResponseFile.read()
        except SSL.SysCallError, e:
            return environs.getVar("httpRequestHandler").outputAlert(
                "No SOAP answer: SSL.SysCallError%s" % str(e))
        return soapResponseMsg

    def getCertificateFilePath(self):
        nodes = self.evaluateXpath("yep:certificateFile")
        if nodes:
            location = nodes[0].content.strip()
            return self.convertRelativeLocationToAbsolutePath(location)
        else:
            return None

    def getLassoServerDump(self):
        if self._lassoServerDump is None:
            lassoServer = lasso.Server(
                self.getMetadataFilePath(), self.getPublicKeyFilePath(),
                self.getPrivateKeyFilePath(), self.getCertificateFilePath(),
                lasso.signatureMethodRsaSha1)
            
            if not self.getPeerMetadataFilePath("[@id='0']"):
                # old format; only one peer
                lassoServer.addProvider(
                    self.getPeerMetadataFilePath(), self.getPeerPublicKeyFilePath(),
                    self.getPeerCertificationAuthorityCertificateFilePath())
            else:
                # new format, several peer
                i = 0
                while True:
                    qualifier = "[@id='%s']" % i
                    i += 1
                    metadataFilePath = self.getPeerMetadataFilePath(qualifier)
                    if not metadataFilePath:
                        break # the end
                    logs.debug("Added peer metadata: %s" % metadataFilePath)
                    lassoServer.addProvider(
                        self.getPeerMetadataFilePath(qualifier),
                        self.getPeerPublicKeyFilePath(qualifier),
                        self.getPeerCertificationAuthorityCertificateFilePath(qualifier))
                    
            self._lassoServerDump = lassoServer.dump()
        return self._lassoServerDump

    def getMetadata(self):
        nodes = self.evaluateXpath("yep:metadata/@src")
        if not nodes:
            return None
        location = nodes[0].content.strip()
        metadataAbsolutePath = self.convertRelativeLocationToAbsolutePath(location)
        metadataHolder = dataholders.DataHolder(
            pathFragment = metadataAbsolutePath, mimeType = "text/xml", isRootElder = True,
            containedFileSystem = filesystems.PartialFileSystem(metadataAbsolutePath))
        return metadataHolder.getRootElement()

    def getMetadataFilePath(self):
        nodes = self.evaluateXpath("yep:metadata/@src")
        if nodes:
            location = nodes[0].content.strip()
            return self.convertRelativeLocationToAbsolutePath(location)
        else:
            return None

    def getPeerCertificationAuthorityCertificateFilePath(self, qualifier = ""):
        nodes = self.evaluateXpath("yep:peerCACertificateFile%s" % qualifier)
        if nodes:
            location = nodes[0].content.strip()
            return self.convertRelativeLocationToAbsolutePath(location)
        else:
            return None

    def getPeerMetadata(self):
        # FIXME: Temporary element name: yep:peerMetadata
        nodes = self.evaluateXpath("yep:peerMetadata/@src")
        if not nodes:
            return None
        location = nodes[0].content.strip()
        metadataAbsolutePath = self.convertRelativeLocationToAbsolutePath(location)
        metadataHolder = dataholders.DataHolder(
            pathFragment = metadataAbsolutePath, mimeType = "text/xml", isRootElder = True,
            containedFileSystem = filesystems.PartialFileSystem(metadataAbsolutePath))
        return metadataHolder.getRootElement()

    def getPeerMetadataFilePath(self, qualifier = ""):
        nodes = self.evaluateXpath("yep:peerMetadata%s/@src" % qualifier)
        if nodes:
            location = nodes[0].content.strip()
            return self.convertRelativeLocationToAbsolutePath(location)
        else:
            return None

    def getPeerPublicKeyFilePath(self, qualifier = ""):
        nodes = self.evaluateXpath("yep:peerPublicKeyFile%s" % qualifier)
        if nodes:
            location = nodes[0].content.strip()
            return self.convertRelativeLocationToAbsolutePath(location)
        else:
            return None

    def getPrivateKeyFilePath(self):
        nodes = self.evaluateXpath("yep:privateKeyFile")
        if nodes:
            location = nodes[0].content.strip()
            return self.convertRelativeLocationToAbsolutePath(location)
        else:
            return None

    def getPublicKeyFilePath(self):
        nodes = self.evaluateXpath("yep:publicKeyFile")
        if nodes:
            location = nodes[0].content.strip()
            return self.convertRelativeLocationToAbsolutePath(location)
        else:
            return None

    def getResponsesDirectoryAbsolutePath(self):
        nodes = self.evaluateXpath("yep:responsesDirectory")
        if nodes:
            location = nodes[0].content.strip()
            return self.convertRelativeLocationToAbsolutePath(location)
        else:
            return None

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

    def getUsersDirectoryAbsolutePath(self):
        nodes = self.evaluateXpath("yep:usersDirectory")
        if nodes:
            location = nodes[0].content.strip()
            return self.convertRelativeLocationToAbsolutePath(location)
        else:
            return None

    def getSessionsDirectoryAbsolutePath(self):
        nodes = self.evaluateXpath("yep:sessionsDirectory")
        if nodes:
            location = nodes[0].content.strip()
            return self.convertRelativeLocationToAbsolutePath(location)
        else:
            return None

    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("")

        lassoServerDump = self.getLassoServerDump()
        lassoServer = lasso.Server.newFromDump(lassoServerDump)
        lassoLogin = lasso.Login(lassoServer)
        try:
            lassoLogin.initAuthnRequest(lasso.httpMethodRedirect)
        except:
            raise Exception("Lasso login error")

        # Identity provider will ask user to authenticate himself.
        lassoLogin.authnRequest.isPassive = False

        # Identity provider will not ask user to authenticate himself if he has already done it
        # recently.
        # lassoLogin.request.set_forceAuthn(False)
        
        lassoLogin.authnRequest.nameIdPolicy = lasso.libNameIdPolicyTypeFederated
        lassoLogin.authnRequest.consent = lasso.libConsentObtained
        # lassoLogin.request.set_relayState("fake")
        # lassoLogin.request.set_protocolProfile(lasso.libProtocolProfileBrwsArt)

        try:
            lassoLogin.buildAuthnRequestMsg(self.getPeerMetadata().getProviderId())
        except:
            raise Exception("Lasso login error")

        return environs.getVar("httpRequestHandler").outputRedirect(lassoLogin.msgUrl)

    def logout(self):
        """Liberty Alliance service provider method that builds a logout request and sends it to
        the identity provider.

        Handles HTTP GET.
        """

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

        user = environs.getVar("user")
        if user is None:
            raise Exception("FIXME: already logged out")
        lassoIdentityDump = user.getLassoIdentityDump()
        if lassoIdentityDump is None:
            raise Exception("FIXME: Identity has no federation => non Liberty logout")
        session = environs.getVar("session")
        if session is None:
            raise Exception("FIXME: already logged out")
        lassoSessionDump = session.getLassoSessionDump()
        if lassoSessionDump is None:
            raise Exception("FIXME: Session has no authentication assertion => non Liberty logout")
        lassoServerDump = self.getLassoServerDump()
        lassoServer = lasso.Server.newFromDump(lassoServerDump)
        lassoLogout = lasso.Logout(lassoServer, lasso.providerTypeSp)
        lassoLogout.setIdentityFromDump(lassoIdentityDump)
        lassoLogout.setSessionFromDump(lassoSessionDump)
        try:
            lassoLogout.initRequest()
        except:
            raise Exception("Lasso logout error")
        try:
            lassoLogout.buildRequestMsg()
        except:
            raise Exception("Lasso logout error")
        nameIdentifier = lassoLogout.nameIdentifier
        soapResponseMessage = self.callSoap(lassoLogout.msgUrl, lassoLogout.msgBody)
        lassoLogout.processResponseMsg(soapResponseMessage, lasso.httpMethodSoap)
        # Logout doesn't change identity federation => no change to user.
        assert not lassoLogout.isIdentityDirty
        # Logout removes authentication assertion from session
        assert lassoLogout.isSessionDirty
        lassoSession = lassoLogout.session
        if lassoSession is None:
            user.deleteSessionToken()
            user.getDocument().save()
            baseEnviron = environs.get(_level = "handleHttpCommand")
            baseEnviron.setVar("user", None)
            session = environs.getVar("session")
            session.getDocument().destroy()
            baseEnviron.setVar("session", None)
        else:
            lassoSessionDump = lassoSession.dump()
            session.setLassoSessionDump(lassoSessionDump)
        # Remove the symbolic link from name identifier to session.
        sessionSymlinksAbsolutePath = self.getSessionsDirectoryAbsolutePath()
        sessionSymlinkName = strings.simplify(nameIdentifier) + ".xml"
        sessionSymlinkAbsolutePath = os.path.join(sessionSymlinksAbsolutePath, sessionSymlinkName)
        try:
            os.remove(sessionSymlinkAbsolutePath)
        except OSError, error:
            logs.warning("Unable to remove symlink %s to session (errno = %s)" % (
                sessionSymlinkAbsolutePath, error.errno))
        # Leave HTTPS.
        nextUrl = locations.removeSessionFromUrl("/")
        # The following instruction doesn't work when going from HTTPS to HTTP.
        # return self.walkToLocation(nextUrl, "GET")
        return environs.getVar("httpRequestHandler").outputRedirect(nextUrl)

##     def singleLogout(self):
##         """Liberty Alliance identity provider method that processes an authentication request
##         coming (indirectly) from a service provider.

##         Handles HTTP GET & POST.
##         """

##         command = environs.getVar("httpCommand")
##         if command not in ("GET", "POST"):
##             raise faults.PathNotFound("")

##         lassoServerDump = self.getLassoServerDump()
##         lassoServer = lasso.Server.new_from_dump(lassoServerDump)
##         lassoIdentity = None
##         account = None
##         user = environs.getVar("user")
##         if user is None:
##             raise "FIXME"
##         account = user.getLibertyAllianceAccount()
##         if account is None:
##             raise "FIXME"
##         lassoIdentityDump = account.getLassoIdentityDump()
##         if lassoIdentityDump is None:
##             raise "FIXME"
##         lassoIdentity = lasso.User.new_from_dump(lassoIdentityDump)
##         lassoLogout = lasso.Logout(lassoServer, lassoIdentity, lasso.providerTypeIdp)
##         lassoLogout.process_request_msg(request_msg, lasso.httpMethodSoap)
## #idplogout.build_response_msg()

    def singleSignOn(self):
        """Liberty Alliance identity provider method that processes an authentication request
        coming (indirectly) from a service provider.

        Handles HTTP GET & POST.
        """

        command = environs.getVar("httpCommand")
        if command not in ("GET", "POST"):
            raise faults.PathNotFound("")

        lassoServerDump = self.getLassoServerDump()
        lassoServer = lasso.Server.newFromDump(lassoServerDump)
        lassoLogin = lasso.Login(lassoServer)
        user = environs.getVar("user")
        if user is not None:
            lassoIdentityDump = user.getLassoIdentityDump()
            if lassoIdentityDump is not None:
                lassoLogin.setIdentityFromDump(lassoIdentityDump)
        session = environs.getVar("session")
        if session is not None:
            lassoSessionDump = session.getLassoSessionDump()
            if lassoSessionDump is not None:
                lassoLogin.setSessionFromDump(lassoSessionDump)
        if command == "GET":
            authnRequestMsg = environs.getVar("httpQuery")
            method = lasso.httpMethodRedirect
        else: # command == "POST"
            # FIXME: We should check if the POST is SOAP. In this case this is a LECP AuthRequest.
            submission = environs.getVar("submission")
            authnRequestMsg = submission.getField("LAREQ")
            method = lasso.httpMethodPost
        try:
            lassoLogin.initFromAuthnRequestMsg(authnRequestMsg, method)
        except:
            raise Exception("Lasso login error")
        logs.debug("ProtocolProfile = %s" % lassoLogin.protocolProfile)
        mustAuthenticate = lassoLogin.mustAuthenticate()
        logs.debug("User must be authenticated = %s" % mustAuthenticate)
        if mustAuthenticate:
            if self.getConfigBoolean("yep:useHttpAuthentication", default = False):
                # HTTP authentication.
                if user is None:
                    return environs.getVar("httpRequestHandler").outputErrorUnauthorized(
                        self.getUriAbsolutePath())
                else:
                    # The user is already authenticated using HTTP authentication.
                    userAuthenticated = True
                    authenticationMethod = "password"
            elif self.getConfigBoolean("yep:useCertificateAuthentication", default = False):
                if user is None:
                    return environs.getVar("httpRequestHandler").outputErrorUnauthorized(
                        self.getUriAbsolutePath())
                else:
                    userAuthenticated = True
                    authenticationMethod = "certificate"
            elif self.getConfigString("yep:useCustomAuthentication", default = None):
                # FIXME: Authenticate the user and set the right authenticationMethod.
                #
                # it should probably create a session with lassoServerDump
                # and authnRequestMsg then redirect to a login page; either
                # login/password or "please enter smartcard" page; then it
                # would call back this method on submit.
                #
                # TODO: this could be refactored in core/
                
                nodes, nodesOwner = self.getConfigNodesAndOwner("yep:useCustomAuthentication")

                nsUri = nodes[0].prop("ns") or namespaces.yep.uri
                elementName = nodes[0].content.strip()
                klass = modules.getElementClass(nsUri, elementName)
                auth = klass()
                try:
                    userAuthenticated, authenticationMethod = auth.singleSignOn(
                        self.getDataHolder(), lassoServerDump, authnRequestMsg)
                except faults.AbortFault:
                    return
            else:
                raise "No authentication method defined!"
        else:
            userAuthenticated = True
            authenticationMethod = "password" # FIXME

        return self.singleSignOnPart2(userAuthenticated, authenticationMethod, lassoLogin)

    def singleSignOnPart2(self, userAuthenticated, authenticationMethod, lassoLogin):
        if authenticationMethod == "password":
            lassoAuthenticationMethod = lasso.samlAuthenticationMethodPassword
        elif authenticationMethod == "certificate":
            lassoAuthenticationMethod = lasso.samlAuthenticationMethodSoftwarePki
        else:
            logs.debug('Unknown authentication method = "%s"' % authenticationMethod)
        if lassoLogin.protocolProfile == lasso.loginProtocolProfileBrwsArt:
            # We can send the artifact either using a redirect or a POST. It is an IDP choice,
            # but redirect is better than POST (no need for a page with a form).
            useRedirectForArtifact = True
            if useRedirectForArtifact:
                try:
                    lassoLogin.buildArtifactMsg(
                        userAuthenticated, lassoAuthenticationMethod,
                        "2005-05-03T16:12:00Z", # FIXME: reauthenticateOnOrAfter
                        lasso.httpMethodRedirect)
                except:
                    raise Exception("Lasso login error")
                logs.debug("msgUrl = %s; assertionArtifact = %s; response_dump = %s" % (
                    lassoLogin.msgUrl, lassoLogin.assertionArtifact,
                    lassoLogin.assertion.dump()))
                nameIdentifier = lassoLogin.nameIdentifier

                # Save the new or updated Lasso identity into user.
                user = environs.getVar("user")
                if lassoLogin.isIdentityDirty:
                    user.setLassoIdentityDump(lassoLogin.identity.dump())
                user.getDocument().save()

                # Create a symbolic link to user, to allow to retrieve it using name identifier.
                userSymlinksAbsolutePath = self.getUsersDirectoryAbsolutePath()
                userSymlinkName = strings.simplify(nameIdentifier) + ".xml"
                userSymlinkAbsolutePath = os.path.join(userSymlinksAbsolutePath, userSymlinkName)
                try:
                    userPath = os.readlink(userSymlinkAbsolutePath)
                except OSError, error:
                    if error.errno not in (errno.ENOENT, errno.EACCES):
                        raise
                    userAbsolutePath = None
                else:
                    userAbsolutePath = os.path.join(userSymlinksAbsolutePath, userPath)
                if userAbsolutePath != user.getAbsolutePath():
                    if not os.access(userSymlinksAbsolutePath, os.F_OK):
                        os.makedirs(userSymlinksAbsolutePath)
                    # os.access(userSymlinkAbsolutePath, os.F_OK) returns False for broken
                    # symlinks, so we can't use it to test existence of symlink.
                    try:
                        os.remove(userSymlinkAbsolutePath)
                    except OSError, error:
                        if error.errno != errno.ENOENT:
                            raise
                    # FIXME: Use a relative path instead of an absolute one for symbolic link.
                    os.symlink(user.getAbsolutePath(), userSymlinkAbsolutePath)

                # Save the new or updated Lasso session into session.
                session = environs.getVar("session")
                if lassoLogin.isSessionDirty:
                    session.setLassoSessionDump(lassoLogin.session.dump())
                session.getDocument().save()

                # Create a symbolic link to session, to allow to retrieve it using name identifier.
                sessionSymlinksAbsolutePath = self.getSessionsDirectoryAbsolutePath()
                sessionSymlinkName = strings.simplify(nameIdentifier) + ".xml"
                sessionSymlinkAbsolutePath = os.path.join(
                    sessionSymlinksAbsolutePath, sessionSymlinkName)
                try:
                    sessionPath = os.readlink(sessionSymlinkAbsolutePath)
                except OSError, error:
                    if error.errno not in (errno.ENOENT, errno.EACCES):
                        raise
                    sessionAbsolutePath = None
                else:
                    sessionAbsolutePath = os.path.join(sessionSymlinksAbsolutePath, sessionPath)
                if sessionAbsolutePath != session.getAbsolutePath():
                    if not os.access(sessionSymlinksAbsolutePath, os.F_OK):
                        os.makedirs(sessionSymlinksAbsolutePath)
                    # os.access(sessionSymlinkAbsolutePath, os.F_OK) returns False for broken
                    # symlinks, so we can't use it to test existence of symlink.
                    try:
                        os.remove(sessionSymlinkAbsolutePath)
                    except OSError, error:
                        if error.errno != errno.ENOENT:
                            raise
                    # FIXME: Use a relative path instead of an absolute one for symbolic link.
                    os.symlink(session.getAbsolutePath(), sessionSymlinkAbsolutePath)

                # Save response dump, to retrieve it later, when receiving a request with the
                # same artifact.
                responsesPath = self.getResponsesDirectoryAbsolutePath()
                responsesHolder = directories.DirectoryHolder(
                    pathFragment = responsesPath, previous = environs.getVar("rootStation"),
                    isRootElder = True,
                    containedFileSystem = filesystems.PartialFileSystem(responsesPath))
                responseFeature = modules.getElementFeature(
                    namespaces.yep.uri, "libertyAllianceResponse")
                responseHolder = responseFeature.newXmlHolder(
                    responsesHolder,
                    pathFragment = strings.simplify(lassoLogin.assertionArtifact) + ".xml")
                response = responseHolder.getRootElement()
                response.setArtifact(lassoLogin.assertionArtifact)
                response.setResponseDump(lassoLogin.assertion.dump())
                response.getDocument().save()

                return environs.getVar("httpRequestHandler").outputRedirect(
                    lassoLogin.msgUrl)
            else:
                # Send artifact using HTTP POST.
                try:
                    lassoLogin.buildArtifactMsg(
                        userAuthenticated, lassoAuthenticationMethod,
                        "2005-05-03T16:12:00Z", # FIXME: reauthenticateOnOrAfter
                        lasso.httpMethodPost)
                except:
                    raise Exception("Lasso login error")
                logs.debug("msgUrl = %s; msgBody = %s; msg_relayState = %s; "
                           "assertionArtifact = %s; response_dump = %s" % (
                    lassoLogin.msgUrl, lassoLogin.msgBody, lassoLogin.msgRelayState,
                    lassoLogin.assertionArtifact, lassoLogin.assertion.dump()))
                raise "FIXME"
        else: # lassoLogin.protocolProfile == lasso.loginProtocolProfileBrwsPost:
            try:
                lassoLogin.buildAuthnResponseMsg(
                    userAuthenticated, lassoAuthenticationMethod,
                    "2005-05-03T16:12:00Z", # FIXME: reauthenticateOnOrAfter
                    )
            except:
                raise Exception("Lasso login error")
            logs.debug("msgUrl = %s; msgBody = %s; msg_relayState = %s" % (
                lassoLogin.msgUrl, lassoLogin.msgBody, lassoLogin.msgRelayState))
            raise "FIXME"
        raise "TODO"

    def soapEndpoint(self):
        """Liberty Alliance identity & service provider SOAP endpoint.

        Handles HTTP POST.
        """

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

        httpRequestHandler = environs.getVar("httpRequestHandler")
        submission = environs.getVar("submission")
        soapRequestMessage = submission.readFile()
        lassoServerDump = self.getLassoServerDump()
        lassoServer = lasso.Server.newFromDump(lassoServerDump)

        requestType = lasso.getRequestTypeFromSoapMsg(soapRequestMessage)
        if requestType == lasso.requestTypeLogin:
            lassoLogin = lasso.Login(lassoServer)
            try:
                lassoLogin.processRequestMsg(soapRequestMessage)
            except:
                raise Exception("Lasso login error")

            # Retrieve response with artifact. Reply to SOAP request using the response dump, then
            # destroy response.
            responsesPath = self.getResponsesDirectoryAbsolutePath()
            responsesHolder = directories.DirectoryHolder(
                pathFragment = responsesPath, previous = environs.getVar("rootStation"),
                isRootElder = True,
                containedFileSystem = filesystems.PartialFileSystem(responsesPath))
            responseLocalId = responsesHolder.getItemLocalId(strings.simplify(
                lassoLogin.assertionArtifact))
            if responseLocalId is None:
                responseHolder = None
            else:
                responseHolder = responsesHolder.getItem(responseLocalId)
            if responseHolder:
                response = responseHolder.getRootElement()
                lassoLogin.setAssertionFromDump(response.getResponseDump())
                responseHolder.destroy()
            lassoLogin.buildResponseMsg()
            soapResponseMsg = lassoLogin.msgBody
        elif requestType == lasso.requestTypeLogout:
            lassoLogout = lasso.Logout(lassoServer, lasso.providerTypeIdp)
            try:
                lassoLogout.processRequestMsg(soapRequestMessage, lasso.httpMethodSoap)
            except:
                raise Exception("Lasso logout error")
            nameIdentifier = lassoLogout.nameIdentifier

            # Retrieve identity using name identifier.
            user = None
            userSymlinksAbsolutePath = self.getUsersDirectoryAbsolutePath()
            userSymlinkName = strings.simplify(nameIdentifier) + ".xml"
            userSymlinkAbsolutePath = os.path.join(userSymlinksAbsolutePath, userSymlinkName)
            try:
                userPath = os.readlink(userSymlinkAbsolutePath)
            except OSError, error:
                logs.error("Unable to read symlink %s to session (errno = %s)" % (
                    sessionSymlinkAbsolutePath, error.errno))
                raise "FIXME: Do what? Return what?"
            userAbsolutePath = os.path.join(userSymlinksAbsolutePath, userPath)
            try:
                userHolder = dataholders.DataHolder(
                    pathFragment = userAbsolutePath, mimeType = "text/xml", isRootElder = True,
                    containedFileSystem = filesystems.PartialFileSystem(userAbsolutePath))
            except IOError, error:
                logs.error('Unable to read user at path "%s" (errno = %s)' % (
                    userAbsolutePath, error.errno))
                raise "FIXME: Do what? Return what?"
            # Some times, users are stored in independant files; some times they are
            # embedded inside accounts.
            accountOrUser = userHolder.getRootElement()
            user = accountOrUser.getUser()
            if user is None:
                logs.error("Empty user")
                raise "FIXME: Do what? Return what?"
            lassoIdentityDump = user.getLassoIdentityDump()
            if lassoIdentityDump is  None:
                logs.error("Empty identity dump in user")
                raise "FIXME: Do what? Return what?"
            lassoLogout.setIdentityFromDump(lassoIdentityDump)

            # Retrieve session using name identifier.
            session = None
            sessionSymlinksAbsolutePath = self.getSessionsDirectoryAbsolutePath()
            sessionSymlinkName = strings.simplify(nameIdentifier) + ".xml"
            sessionSymlinkAbsolutePath = os.path.join(
                sessionSymlinksAbsolutePath, sessionSymlinkName)
            try:
                sessionPath = os.readlink(sessionSymlinkAbsolutePath)
            except OSError, error:
                logs.error("Unable to read symlink %s to session (errno = %s)" % (
                    sessionSymlinkAbsolutePath, error.errno))
                raise "FIXME: Do what? Return what?"
            sessionAbsolutePath = os.path.join(sessionSymlinksAbsolutePath, sessionPath)
            try:
                sessionHolder = dataholders.DataHolder(
                    pathFragment = sessionAbsolutePath, mimeType = "text/xml", isRootElder = True,
                    containedFileSystem = filesystems.PartialFileSystem(sessionAbsolutePath))
            except IOError, error:
                logs.error('Unable to read session at path "%s" (errno = %s)' % (
                    sessionAbsolutePath, error.errno))
                raise "FIXME: Do what? Return what?"
            foundSession = sessionHolder.getRootElement()
            if foundSession.validate():
                session = foundSession
            if session is None:
                logs.error("Empty session")
                raise "FIXME: Do what? Return what?"
            lassoSessionDump = session.getLassoSessionDump()
            if lassoSessionDump is None:
                logs.error("Empty session dump")
                raise "FIXME: Do what? Return what?"
            lassoLogout.setSessionFromDump(lassoSessionDump)

            try:
                lassoLogout.validateRequest()
            except:
                raise Exception("Lasso logout error")

            # Logout doesn't change identity federation => no change to user.
            assert not lassoLogout.isIdentityDirty
            # Logout removes authentication assertion from session
            assert lassoLogout.isSessionDirty
            lassoSession = lassoLogout.session
            if lassoSession is None:
                user.deleteSessionToken()
                user.getDocument().save()
                assert environs.getVar("user") is None
                #baseEnviron = environs.get(_level = "handleHttpCommand")
                #baseEnviron.setVar("user", None)
                session.getDocument().destroy()
                assert environs.getVar("session") is None
                #baseEnviron.setVar("session", None)
            else:
                lassoSessionDump = lassoSession.dump()
                session.setLassoSessionDump(lassoSessionDump)
            # Remove the symbolic link from name identifier to session.
            sessionSymlinksAbsolutePath = self.getSessionsDirectoryAbsolutePath()
            sessionSymlinkName = strings.simplify(nameIdentifier) + ".xml"
            sessionSymlinkAbsolutePath = os.path.join(
                sessionSymlinksAbsolutePath, sessionSymlinkName)
            try:
                os.remove(sessionSymlinkAbsolutePath)
            except OSError, error:
                logs.warning("Unable to remove symlink %s to session (errno = %s)" % (
                    sessionSymlinkAbsolutePath, error.errno))

            authorization = httpRequestHandler.headers.get("authorization")
            if self.getConfigBoolean("yep:useHttpAuthentication", default = False) \
                   and authorization:
                # Since HTTP authentication provides no way to logout,  we send a status
                # Unauthorized to force the user to press the cancel button. But instead of
                # sending an error page immediately, we send the real page, so the user will see
                # the page instead of an error message.
                environs.setVar("httpAuthenticationLogoutTrick", True)
            # Tell each other service provider to logout the user.
            otherProviderId = lassoLogout.getNextProviderID()
            while otherProviderId is not None:
                lassoLogout.initRequest(otherProviderId)
                lassoLogout.buildRequestMsg()
                logs.debug("IDP logout: SOAP endpoint = %s; soapRequestMsg = %s" % (
                    lassoLogout.msgUrl, lassoLogout.msgBody))
                soapResponseMessage = self.callSoap(lassoLogout.msgUrl, lassoLogout.msgBody)
                try:
                    lassoLogout.processResponseMsg(
                        soapResponseMessage, lasso.httpMethodSoap)
                except:
                    raise Exception("Lasso logout error %s" % "?") #error.code)
                otherProviderId = lassoLogout.getNextProviderID()
            try:
                lassoLogout.buildResponseMsg()
            except:
                raise Exception("Lasso logout error")
            soapResponseMsg = lassoLogout.msgBody
        elif requestType == lasso.requestTypeDefederation:
            raise "FIXME: To do."
        elif requestType == lasso.requestTypeRegisterNameIdentifier:
            raise "FIXME: To do."
        elif requestType == lasso.requestTypeNameIdentifierMapping:
            raise "FIXME: To do."
        else:
            raise Exception("Unknown request type = %s" % requestType)
        httpRequestHandler.outputData(soapResponseMsg, mimeType = "text/xml")


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)
