/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2006 Sun Microsystems Inc. All Rights Reserved
 *
 * The contents of this file are subject to the terms
 * of the Common Development and Distribution License
 * (the License). You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the License at
 * https://opensso.dev.java.net/public/CDDLv1.0.html or
 * opensso/legal/CDDLv1.0.txt
 * See the License for the specific language governing
 * permission and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL
 * Header Notice in each file and include the License file
 * at opensso/legal/CDDLv1.0.txt.
 * If applicable, add the following below the CDDL Header,
 * with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * $Id: SecurityTokenManagerClient.java,v 1.9 2008/08/19 19:11:09 veiming Exp $
 *
 * Portions Copyright 2013-2015 ForgeRock AS.
 */

package com.sun.identity.liberty.ws.security;

import com.sun.identity.shared.locale.Locale;
import com.sun.identity.shared.configuration.SystemPropertiesManager;
import com.sun.identity.shared.xml.XMLUtils;

import com.sun.identity.common.SystemConfigurationUtil;
import com.sun.identity.shared.encode.Base64;
import com.sun.identity.shared.encode.Base64;

import com.sun.identity.liberty.ws.common.wsse.BinarySecurityToken;
import com.sun.identity.liberty.ws.disco.EncryptedResourceID;

import com.sun.identity.plugin.session.SessionManager;
import com.sun.identity.saml.assertion.Assertion;
import com.sun.identity.saml.assertion.AssertionIDReference;
import com.sun.identity.saml.assertion.Attribute;
import com.sun.identity.saml.assertion.NameIdentifier;
import com.sun.identity.saml.common.SAMLConstants;
import com.sun.identity.saml.common.SAMLException;
import com.sun.identity.saml.common.SAMLUtils;
import com.sun.identity.shared.jaxrpc.SOAPClient;
import com.sun.identity.saml.protocol.AssertionArtifact;

import java.net.InetAddress;
import java.net.URL;

import java.rmi.RemoteException;
import java.rmi.ServerException;

import java.security.cert.X509Certificate;

import java.util.Iterator;
import java.util.ResourceBundle;

import javax.xml.soap.SOAPException;

/**
 * The class <code>SecurityTokenManagerClient</code> is a <code>final</code> 
 * class that provides interfaces to create, get and destroy
 * <code>Assertion</code>s.
 * <p>
 * The class provides mechanisms to manage the <code>Assertion</code>s either
 * locally (i.e., within the same JVM process) or remotely on another instance
 * of OpenAM. The default constructor will manage the <code>
 * Assertion</code>s locally if it detects SAML web services running locally,
 * else will use on of the configured OpenAM. The constructor which
 * accepts an <code>URL</code> will always use the URL to manage the assertions.
 * <p>
 * Having obtained an instance of <code>AssertionManagerClient</code>, its
 * methods can be called to create/get <code>Assertion</code>, and 
 * <code>AssertionArtifact</code>, and to obtain decision from an 
 * <code>Query</code>.
 *
 * @supported.api
 */
public final class SecurityTokenManagerClient {

    // Service name in naming
    private static String SERVICE_NAME = "securitytokenmanager";

    // Flag to determine if AssertionManager is local or remote
    private static boolean checkedForLocal;
    private static boolean isLocal;
    private boolean useLocal;

    // Remote JAX-RPC server for objects that use default constructor
    private static SOAPClient remoteStub;

    // If local pointer to SecurityTokenManager instance
    private SecurityTokenManager securityTokenManager;
    private String ssoToken = null;

    // JAX-RPC remote stub
    private SOAPClient stub;

    static ResourceBundle bundle = 
                Locale.getInstallResourceBundle("libLibertySecurity");
    /**
     * Returns an instance of <code>SecurityTokenManagerClient</code>
     *
     * @param credential credential of the caller used to see
     *        if access to this security token manager client is allowed.
     * @throws SecurityTokenException if unable to access the
     *         the security token manager client.
     */
    public SecurityTokenManagerClient(Object credential)
                                      throws SecurityTokenException {
        if (!checkedForLocal) {
            try {
                // Construct the URL for local server.
                remoteStub = getServiceEndPoint(
                    SystemPropertiesManager.get(SAMLConstants.SERVER_PROTOCOL),
                    SystemPropertiesManager.get(SAMLConstants.SERVER_HOST),
                    SystemPropertiesManager.get(SAMLConstants.SERVER_PORT),
                    SystemPropertiesManager.get(SAMLConstants.SERVER_URI));
                remoteStub.send("checkForLocal", null, null, null);
                if (SecurityTokenManagerImpl.isLocal) {
                    isLocal = true;
                    SecurityTokenManager.debug.warning(
                            "STMC(): Using local service");
                    securityTokenManager = new SecurityTokenManager(credential);
                }
                checkedForLocal = true;
            } catch (Exception e) {
                checkedForLocal = true;
                if (SecurityTokenManager.debug.warningEnabled()) {
                    SecurityTokenManager.debug.warning(
                            "SecurityTokenManagerClient()Exception", e);
                }
                throw (new SecurityTokenException(e.getMessage()));
            }
        }
        if (isLocal) {
            useLocal = true;
        } else {
            // Use the remoteStub if set
            stub = remoteStub;
            try {
                ssoToken =
                    SessionManager.getProvider().getSessionID(credential);
                stub.send("initialization", ssoToken, null, ssoToken); 
            } catch (Exception e) {
                if (SecurityTokenManager.debug.warningEnabled()) {
                    SecurityTokenManager.debug.warning(
                            "SecurityTokenManagerClient()Exception", e);
                }
                throw (new SecurityTokenException(e.getMessage()));
            }
        }
    }
    
    /**
     * Returns an instance of <code>SecurityTokenManagerClient</code>
     * that will use the provided <code>URL</code> for the management
     * of security tokens.
     *
     * @param url the <code>SecurityTokenManagerClient</code> service URL that
     *        will be used to get <code>BinarySecurityToken</code> and
     *	      <code>SAMLSecurityToken</code>.
     * @param credential credential of the caller used to see
     *        if access to this security token manager client is allowed.
     * @throws SecurityTokenException if unable to access the
     *         the security token manager client.
     */
    public SecurityTokenManagerClient(String url, Object credential)
        throws SecurityTokenException {
        try {
	    // Construct the JAX-RPC stub and set the URL endpoint
            ssoToken = SessionManager.getProvider().getSessionID(credential);
            String[] urls = {url};
            stub = new SOAPClient(urls);
            stub.send("initialization", ssoToken, null, ssoToken);
            useLocal = false;
        } catch (Exception e) {
            if (SecurityTokenManager.debug.warningEnabled()) {
                SecurityTokenManager.debug.warning("STMC() Exception", e);
            }
            throw (new SecurityTokenException(e.getMessage()));
        }
    }

    // Private method to get the service endpoint URL
    private static SOAPClient getServiceEndPoint(String protocol,
        String hostname, String port, String uri) throws Exception {
        // Obtain the URL for the service endpoint
        int intPort = Integer.parseInt(port);
        URL weburl = SystemConfigurationUtil.getServiceURL(SERVICE_NAME,
            protocol, hostname, intPort, uri);
        String iurl = weburl.toString();
        if (SecurityTokenManager.debug.messageEnabled()) {
            SecurityTokenManager.debug.message(
                "SecurityTokenManagerClient with URL: " + iurl);
        }
        String[] urls = {iurl};
        return new SOAPClient(urls);
    }

    private static SOAPClient getRemoteStub()
        throws SecurityTokenException {
	boolean foundServer = false;
	Exception ee = null;
	SOAPClient remoteStub = null;
	try {
	    // Get the list of platform servers
	    Iterator serverList =
		SystemConfigurationUtil.getServerList().iterator();

	    // Get a server that is responding
	    while (serverList.hasNext() && !foundServer) {
		URL u = new URL((String) serverList.next());
		remoteStub = getServiceEndPoint(u.getProtocol(), u.getHost(),
		    Integer.toString(u.getPort()), u.getPath());
		// Check if the server is active
		try {
		    // this call will throw an exception if server is down
		    remoteStub.send("checkForLocal", null, null, null);
		    if (SecurityTokenManager.debug.messageEnabled()) {
			SecurityTokenManager.debug.message(
			    "STMC(): Using the remote URL: " + u.toString());
		    }
		    foundServer = true;
		    if (SecurityTokenManager.debug.warningEnabled()) {
			SecurityTokenManager.debug.warning(
			    "STMC:getRemoteStub: remote server being used: "
			    + u.toString());
		    }
		} catch (Exception e) {
		    ee = e;
		    if (SecurityTokenManager.debug.warningEnabled()) {
			SecurityTokenManager.debug.warning(
			    "STMC:getRemoteStub: server (" +
			    u.toString() + ") error: ", e);
		    }
		}
	    }
	} catch (Exception f) {
	    ee = f;
	    if (SecurityTokenManager.debug.warningEnabled()) {
		SecurityTokenManager.debug.warning(
			"STMC:getRemoteStub: generic error: ", f);
	    }
	}
	
	if (!foundServer) {
	    // No valid server found. Return the last exception
	    if (ee != null) {
		throw (new SecurityTokenException(ee.getMessage()));
	    } else {
		throw (new SecurityTokenException(
		    bundle.getString("serverNotFound")));
	    }
	}
	return (remoteStub);
    }

    /**
     * Sets the alias of the certificate used for issuing <code>WSS</code>
     * token, i.e. <code>WSS</code> <code>X509</code> Token, <code>WSS</code>
     * SAML Token. If the <code>certAlias</code> is never set, a default
     * certificate will be used for issuing <code>WSS</code> tokens.
     *
     * @param certAlias String alias name for the certificate.
     * @throws SecurityTokenException if certificate for the
     *            <code>certAlias</code> could not be found in key store.
     * 
     * @supported.api
     */
    public void setCertAlias(java.lang.String certAlias)
        throws SecurityTokenException {
        if (useLocal) {
            securityTokenManager.setCertAlias(certAlias);
        } else {
            try {
                Object[] obj = {certAlias, Boolean.TRUE};
                stub.send("setCertificate", obj, null, ssoToken);
	    } catch (Exception e) {
		if (SecurityTokenManager.debug.warningEnabled()) {
		    SecurityTokenManager.debug.warning(
			"STMC:setCertAlias()", e);
		}
		throw (new SecurityTokenException(e.getMessage()));
	    }
	}
    }

    /**
     * Sets the  certificate used for issuing <code>WSS</code> token, i.e.
     * <code>WSS</code> <code>X509</code> Token, <code>WSS</code> SAML Token.
     * If the certificate is never set, a default certificate will
     * be used for issuing <code>WSS</code> tokens
     *
     * @param cert <code>X509</code> certificate
     * @throws SecurityTokenException if could not set Certificate.
     */
    public void setCertificate(X509Certificate cert)
        throws SecurityTokenException {
	if (useLocal) {
	    securityTokenManager.setCertificate(cert);
	} else {
	    try {
		String certString = Base64.encode(cert.getEncoded());
                Object[] obj = {certString, Boolean.FALSE};
                stub.send("setCertificate", obj, null, ssoToken);
	    } catch (Exception e) {
		if (SecurityTokenManager.debug.warningEnabled()) {
		    SecurityTokenManager.debug.warning(
			"STMC:setCertificate()", e);
		}
		throw (new SecurityTokenException(e.getMessage()));
	    }
	}
    }

    /**
     * Gets the <code>X509</code> certificate Token.
     *
     * @return <code>X509</code> certificate Token.
     * @throws SecurityTokenException if the binary security token could
     * not be obtained.
     */
    public BinarySecurityToken getX509CertificateToken()
                                      throws SecurityTokenException {
	if (useLocal) {
	    return securityTokenManager.getX509CertificateToken();
	} 

	String bst = null;
	try {
            bst = (String) stub.send("getX509CertificateToken", null, null,
                ssoToken);
            return (new BinarySecurityToken(XMLUtils.toDOMDocument(bst,
                    SecurityTokenManager.debug).getDocumentElement()));
	} catch (Exception e) {
	    if (SecurityTokenManager.debug.warningEnabled()) {
		SecurityTokenManager.debug.warning(
		    "STMC:getX509CertificateToken()", e);
	    }
	    throw (new SecurityTokenException(e.getMessage()));
	}
    }

    /**
     * Creates a SAML Assertion for message authentication.
     *
     * @param senderIdentity name identifier of the sender.
     * @return Assertion which contains an <code>AuthenticationStatement</code>.
     * @throws SecurityTokenException if the assertion could not be obtained.
     * @throws SAMLException if unable to generate the SAML Assertion. 
     */
    public SecurityAssertion getSAMLAuthenticationToken(
            NameIdentifier senderIdentity)
            throws SecurityTokenException, SAMLException {
	if (useLocal) {
	    return (securityTokenManager.getSAMLAuthenticationToken(
			senderIdentity));
	}

	try {
            String ni = senderIdentity.toString(true, true);
            String assertion = (String) stub.send("getSAMLAuthenticationToken", 
                ni, null, ssoToken);
            return (new SecurityAssertion(XMLUtils.toDOMDocument(assertion,
                    SecurityTokenManager.debug).getDocumentElement()));
	} catch (Exception re) {
	    if (SecurityTokenManager.debug.warningEnabled()) {
		SecurityTokenManager.debug.warning(
		    "STMC:getSAMLAuthenticationToken()", re);
	    }
	    throw (new SAMLException(re.getMessage()));
	}
    }

    /**
     * Creates a SAML Assertion for message authorization, the assertion could
     * optionally contain an <code>AuthenticationStatement</code> which will be
     * used for message authentication.
     *
     * @param senderIdentity name identifier of the sender.
     * @param invocatorSession <code>SessionContext</code> of  the invocation 
     *        identity, it is normally obtained by the credential reference in 
     *        the SAML <code>AttributeDesignator</code> for discovery resource 
     *        offering which is part of the liberty <code>ID-FF</code> 
     *        <code>AuthenResponse</code>.
     * @param resourceID id for the resource to be accessed.
     * @param includeAuthN if true, include an 
     *        <code>AutheticationStatement</code> in the Assertion which will be 
     *        used for message authentication. 
     * @param includeResourceAccessStatement if true, a 
     *        <code>ResourceAccessStatement</code>
     *        will be included in the Assertion 
     *        (for <code>AuthorizeRequester</code> directive). If false, 
     *        a <code>SessionContextStatement</code> will be included in the 
     *        Assertion (for <code>AuthenticationSessionContext</code> 
     *        directive). In the case when both <code>AuthorizeRequester</code> 
     *        and <code>AuthenticationSessionContext</code> directive need to be
     *        handled, use "true" as parameter here since the 
     *        <code>SessionContext</code> will always be included in the 
     *        <code>ResourceAccessStatement</code>.
     * @param recipientProviderID recipient's provider ID.
     * @return the <code>SecurityAssertion</code> object.
     * @throws SecurityTokenException if the assertion could not be obtained.
     * @throws SAMLException if unable to generate the SAML Assertion.
     */
    public SecurityAssertion getSAMLAuthorizationToken(
            NameIdentifier senderIdentity,
            SessionContext invocatorSession,
            String resourceID,
            boolean includeAuthN,
            boolean includeResourceAccessStatement,
            String recipientProviderID)
            throws SecurityTokenException, SAMLException {
	if (useLocal) {
	    return (securityTokenManager.getSAMLAuthorizationToken(
			senderIdentity, invocatorSession, resourceID,
			includeAuthN, includeResourceAccessStatement,
                        recipientProviderID));
	}

	try {
            String ni = senderIdentity.toString(true, true);
            String sc = invocatorSession.toXMLString(true, true);
            Object[] obj = {ni, sc, resourceID, Boolean.FALSE, 
                Boolean.valueOf(includeAuthN),
                Boolean.valueOf(includeResourceAccessStatement),
                recipientProviderID};
            String assertion = (String) stub.send("getSAMLAuthorizationToken",
                obj, null, ssoToken);
            return (new SecurityAssertion(XMLUtils.toDOMDocument(assertion,
		    SecurityTokenManager.debug).getDocumentElement()));
	} catch (Exception e) {
	    if (SecurityTokenManager.debug.warningEnabled()) {
		SecurityTokenManager.debug.warning(
			"STMC:createAssertionArtifact:", e);
	    }
	    throw (new SecurityTokenException(e.getMessage()));
	}
    }

    /**
     * Creates a SAML Assertion for message authorization, the assertion could
     * optionally contain an <code>AuthenticationStatement</code> which will be
     * used for message authentication.
     *
     * @param senderIdentity name identifier of the sender.
     * @param invocatorSession <code>SessionContext</code> of  the invocation 
     *        identity, it is normally obtained by the credential reference in
     *        the SAML <code>AttributeDesignator</code> for discovery resource   
     *        offering which is part of the liberty <code>ID-FF</code> 
     *        <code>AuthenResponse</code>.
     * @param encResourceID Encrypted ID for the resource to be accessed.
     * @param includeAuthN if true, include an 
     *        <code>AutheticationStatement</code> in the
     *        Assertion which will be used for message authentication. 
     * @param includeResourceAccessStatement if true, 
     *        a <code>ResourceAccessStatement</code> will be included in the 
     *        Assertion (for <code>AuthorizeRequester</code> directive). If
     *        false, a <code>SessionContextStatement</code> will be included in 
     *        the Assertion (for <code>AuthenticationSessionContext</code> 
     *        directive). In the case when both <code>AuthorizeRequester</code> 
     *        and <code>AuthenticationSessionContext</code> directive need to be
     *        handled, use "true" as parameter here since the 
     *        <code>SessionContext</code> will always be included in the 
     *        <code>ResourceAccessStatement</code>.
     * @param recipientProviderID recipient's provider ID.
     * @return the <code>SecurityAssertion</code> object.
     * @throws SecurityTokenException if the assertion could not be obtained.
     * @throws SAMLException if unable to generate the SAML Assertion.
     */
    public SecurityAssertion getSAMLAuthorizationToken(
		NameIdentifier senderIdentity,
		SessionContext invocatorSession,
		EncryptedResourceID encResourceID,
		boolean includeAuthN,
		boolean includeResourceAccessStatement,
                String recipientProviderID)
	throws SecurityTokenException, SAMLException {
        if (useLocal) {
            return (securityTokenManager.getSAMLAuthorizationToken(
                senderIdentity, invocatorSession, encResourceID,
                includeAuthN, includeResourceAccessStatement,
                recipientProviderID));
        }

        String assertion = null;

        try {
            String ni = senderIdentity.toString(true, true);
            String sc = invocatorSession.toXMLString(true, true);
            String resourceID = encResourceID.toString();
            Object[] obj = {ni, sc, resourceID,  Boolean.TRUE,
                Boolean.valueOf(includeAuthN),
                Boolean.valueOf(includeResourceAccessStatement),
                recipientProviderID};
            assertion = (String) stub.send("getSAMLAuthorizationToken",
                obj, null, ssoToken);
            return (new SecurityAssertion(XMLUtils.toDOMDocument(assertion,
                    SecurityTokenManager.debug).getDocumentElement()));
        } catch (Exception e) {
            if (SecurityTokenManager.debug.warningEnabled()) {
                SecurityTokenManager.debug.warning(
                        "STMC:getSAMLAuthorizationToken() ", e);
            }
            throw (new SecurityTokenException(e.getMessage()));
        }
    }
}
