/*
 * OSGAuthorization.java
 *
 * Created on October 18, 2005, 3:20 PM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 *
 * Weigand 6/16/06:  
 *   Modified the VOMSUserInfo class to identify the user of a cert
 #   differently.  The old method was was causing a problem with 
 #   delegated proxies and we now  mirror what Globus does in the gridmap authz
 *   service. The peerSubject was needed on instantiation of the VOMSUserIndo
 *   class.
 */

package org.opensciencegrid.authz.gt4;

import java.io.File;
import java.security.Principal;
import java.util.Arrays;
import java.util.Iterator;
import java.security.cert.X509Certificate;
import java.util.Set;
import javax.security.auth.Subject;
import javax.xml.namespace.QName;
import javax.xml.rpc.handler.MessageContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.globus.gsi.gssapi.GlobusGSSCredentialImpl;
import org.globus.gsi.jaas.GlobusPrincipal;
import org.globus.gsi.jaas.JaasGssUtil;
import org.globus.gsi.jaas.UserNamePrincipal;

import org.globus.wsrf.Resource;
import org.globus.wsrf.ResourceContext;
import org.globus.wsrf.ResourceException;
import org.globus.wsrf.ResourceContextException;
import org.globus.wsrf.config.ConfigException;
import org.globus.wsrf.impl.security.descriptor.SecurityPropertiesHelper;
import org.globus.security.gridmap.GridMap;

import org.globus.wsrf.impl.security.util.AuthUtil; 
import org.globus.wsrf.impl.security.authorization.exceptions.AuthorizationException;
import org.globus.wsrf.impl.security.authorization.exceptions.CloseException;
import org.globus.wsrf.impl.security.authorization.exceptions.InitializeException;
import org.globus.wsrf.impl.security.authorization.exceptions.InvalidPolicyException;
import org.globus.wsrf.security.SecurityManager;
import org.globus.wsrf.security.authorization.PDP;
import org.globus.wsrf.security.authorization.PDPConfig;
import org.opensciencegrid.authz.client.GRIDIdentityMappingServiceClient;
import org.opensciencegrid.authz.common.GridId;
import org.opensciencegrid.authz.common.LocalId;
import org.w3c.dom.Node;

/** This class implements the Globus Callout to call an Identity Mapping Service
 * such as GUMS. For more information about the Globus AuthZ callout mechanism
 * refer to Globus documentation.
 * <p>
 * The entry point for the class is the isPermitted method, that will be called
 * by the GT4 code. initialize will be called once when the Globus container
 * is started.
 *
 * @author Gabriele Carcassi
 */
public class OSGAuthorization implements PDP {
    
    public static final String FILE = "gridmap-file";
    public static final String OBJECT = "gridmapObject";

    String prefix = null;
    String servicePath = null;

    private static Log log =
        LogFactory.getLog(OSGAuthorization.class);

    // ----------------------------------------------------------------------
    public Node getPolicy(Node policy) throws InvalidPolicyException {
        // Not clear what this function should implement:
        // other implementation just return null, so that's what we'll do
        return null;
    }

    // ---------------------------------------------------------------------
    public Node setPolicy(Node policy) throws InvalidPolicyException {
        // Not clear what this function should implement:
        // other implementation just return null, so that's what we'll do
        return null;
    }

    // ---------------------------------------------------------------------
    public String[] getPolicyNames() {
        // Not clear what this function should implement:
        // other implementation just return null, so that's what we'll do
        return null;
    }

    // ---------------------------------------------------------------------
    public boolean isPermitted(Subject peerSubject, MessageContext context,
                               QName op) throws AuthorizationException {
        GridId userGridID = null;
        try {
            log.info("=========== PRIMA GT4 AUTHORIZATION  ===============");
            log.info("GT4 PRIMA OSGAuthorization started for "+AuthUtil.getIdentity(peerSubject));
            String username = null;

            // -- Retrieve a configuration object --
            PRIMAConfiguration conf = PRIMAConfiguration.getInstance();

            // -- Retrieve the client certificate chain --
            X509Certificate[] clientCertChain = retrieveClientCertChain(peerSubject);
            
            // -- Retrieve the server certificate chain --
            log.trace("Retrieving server certificate chain...");
            X509Certificate[] serviceCertChain = retrieveServiceCertChain(context);
            if (log.isTraceEnabled()) {
                if (serviceCertChain == null) {
                    log.warn("No server certificate chain retrieved");
                } else {
                    log.trace("Server certificate chain retrieved: " + Arrays.asList(serviceCertChain));
                }
            }

            // -- retrieve the VOMS information ----
            VOMSUserInfo vomsInfo = VOMSUserInfo.retrieveVOMSUserInfo(peerSubject, clientCertChain); 
            log.info("PRIMA: Client certificate information: " + vomsInfo);
            
            log.trace("PRIMA: Retrieving GRAM Service DN...");
            String hostDN = retrieveDN(retrieveServiceCredential(context));
            log.trace("PRIMA: GRAM Service DN (" + hostDN + ")");
            
            log.trace("PRIMA: Preparing information for AuthZ server call...");
            userGridID = new GridId();
            userGridID.setHostDN(hostDN);
            userGridID.setUserDN(vomsInfo.getUserDN());
            userGridID.setUserFQAN(vomsInfo.getFirstFQAN());
            userGridID.setUserFQANIssuer(vomsInfo.getVOMSServerDN());

	    log.info("PRIMA: User "+userGridID);

	    /*
	     *
	     * Check cache
	     *
	     */

	    // get resource
	    Resource resource = null;
	    try {
		ResourceContext ctx = ResourceContext
		    .getResourceContext((org.apache.axis.MessageContext)context);
		resource = ctx.getResource();
	    } catch (ResourceContextException exp) {
		// FIXME: quiet catch. need to have more specific exceptions
		// from core.
		log.debug("Error retrieving resource", exp);
		resource = null;
	    } catch (ResourceException exp) {
		// FIXME: quiet catch. need to have more specific exceptions
		// from core.
		log.debug("Error retrieving resource", exp);
		resource = null;
	    }

	    log.debug("Service " + this.servicePath);
	    GridMap gridMap = null;
	    try {
		gridMap = SecurityPropertiesHelper.getGridMap(this.servicePath,
							      resource);
	    } catch (ConfigException exp) {
		log.debug("Error getting gridmap", exp);
		throw new AuthorizationException("Error getting gridmap");
	    }
	    
	    if (gridMap == null) {
		log.info("No gridmap object, call GUMS");
	    } else {
		String[] usernames = gridMap.getUserIDs(AuthUtil.getIdentity(peerSubject));
		if ((usernames == null) || (usernames.length <=0)) {
		    log.info("PRIMA: No cached info found");
		} else {
		    log.info("PRIMA: Found cached user " + usernames[0]);
		    log.trace("PRIMA: Cached nr: " + usernames.length);
		    // --- set local account in gt4 context ----
		    setupReturnContext(peerSubject, usernames[0]);
		    log.info("=========== PRIMA GT4 DONE - CACHED  ===============");
		    return true;
		}
	    }

            // -- set up a couple message strings to insure consistency --
            String userMsg    = new String(" for "+userGridID+" ");
            String serviceMsg = new String(" authz service (" +conf.getImsContact()+") ");
            
            log.info("PRIMA: Contacting "+serviceMsg+"for"+userMsg);

            log.trace("PRIMA: Setting up AuthZ server call stub");
            GRIDIdentityMappingServiceClient authZClient = new GRIDIdentityMappingServiceClient(conf.getImsContact());
            authZClient.setGlobusCredentials(retrieveServiceCredential(context));

            // -- call the GUMS service ---
            log.trace("PRIMA: Calling AuthZ server ("+conf.getImsContact()+")");
            LocalId userLocalID = authZClient.mapCredentials(userGridID);

            // -- user is denied if GUMS didn't return a local account
            if (userLocalID == null) {
              log.warn("PRIMA: DENY -"+userMsg+"from"+serviceMsg);
              return false;
            }

            // --- set local account in gt4 context ----
            setupReturnContext(peerSubject, userLocalID.getUserName());

            // -- user is authorized ---
            log.info("PRIMA: PERMIT -"+userMsg+"from"+serviceMsg+" is assigned local account (" +userLocalID.getUserName()+ ")");
            log.info("=========== PRIMA GT4 AUTHORIZATION DONE - GUMS PERMIT ===============");
            return true;

        } catch (Exception e) {
            log.warn("PRIMA: DENY - User (" + AuthUtil.getIdentity(peerSubject) + ") - prior to contacting service: " + e.getMessage());
            log.info("=========== PRIMA GT4 AUTHORIZATION DONE - GUMS DENY ===============");
            return false;
        }
    }

    // ---------------------------------------------------------------------
    // FIXME: Last parameter is servicePath (by convention)
    public void initialize(PDPConfig pDPConfig, String string, String servicePath_) throws InitializeException {
        log.trace("PRIMA: GT4 OSGAuthorization invoked");
        initializeConfLocation(pDPConfig, string);
        this.servicePath = servicePath_;
        log.debug("service " + this.servicePath);
    }
    
    // ---------------------------------------------------------------------
    private static final String CONF_LOCATION_DEFAULT = "/etc/grid-security/prima-authz.conf";

    // ---------------------------------------------------------------------
    private void initializeConfLocation(PDPConfig pDPConfig, String string) {
        String confLocation = null;
        try {
            confLocation = (String) pDPConfig.getProperty(string, "confLocation");
            if (confLocation == null) {
                log.trace("PRIMA: Using default configuration file '" + CONF_LOCATION_DEFAULT + "'");
                confLocation = CONF_LOCATION_DEFAULT;
                PRIMAConfiguration.setConfLocation(confLocation);
            } else {
                PRIMAConfiguration.setConfLocation(confLocation);
            }
        } catch (Exception e) {
            log.warn("PRIMA: Couldn't retrieve the configuration location from the properties file.");
            confLocation = CONF_LOCATION_DEFAULT;
            PRIMAConfiguration.setConfLocation(confLocation);
        }
    }

    // ---------------------------------------------------------------------
    public void close() throws CloseException {
        // Nothing to close for now
    }
    
    // ---------------------------------------------------------------------
    void setupReturnContext(Subject peerSubject, String username) {
        log.trace("PRIMA: Local account set to '" + username + "' in GT4 security context");
        peerSubject.getPrincipals().add(new UserNamePrincipal(username));
    }
    
    // ---------------------------------------------------------------------
    X509Certificate[] retrieveClientCertChain(Subject peerSubject) {
        log.trace("PRIMA: Retrieving user certificate chain for " + AuthUtil.getIdentity(peerSubject));
        Set publicCreds = peerSubject.getPublicCredentials();
        if (publicCreds == null) {
            log.warn("PRIMA: DENY - No certificate chain found."); 
            throw new RuntimeException("PRIMA: Couldn't retrieve client certificate chain.");
        }
        Iterator iter = publicCreds.iterator();
        while (iter.hasNext()) {
            Object obj = iter.next();
            if (obj instanceof X509Certificate[]) {
                log.debug("PRIMA: Client certificate chain retrieved -" + Arrays.asList((X509Certificate[]) obj));
                return (X509Certificate[]) obj;
            }
            log.warn("PRIMA: Object " + obj + " is not a certificate chain");
        }
        log.warn("PRIMA: No certificate chain found: getPublicCredentials didn't contain any X509Certificate[] objects");
            throw new RuntimeException("PRIMA: Couldn't retrieve client certificate chain.");
    }
    
    // ---------------------------------------------------------------------
    X509Certificate[] retrieveServiceCertChain(MessageContext context) throws AuthorizationException {
        org.apache.axis.MessageContext ctx = (org.apache.axis.MessageContext) context;
        SecurityManager manager = SecurityManager.getManager(ctx);
        Subject systemSubject = null;
        try {
            systemSubject = manager.getSystemSubject();
        } catch (org.globus.wsrf.security.SecurityException e) {
            log.warn("No System credentials were found", e);
            throw new AuthorizationException("No System credentials were found: " + e.getMessage() , e);
        }
        GlobusGSSCredentialImpl credential = (GlobusGSSCredentialImpl)JaasGssUtil.getCredential(systemSubject);
        return credential.getCertificateChain();
    }

    // ---------------------------------------------------------------------
    GlobusGSSCredentialImpl retrieveServiceCredential(MessageContext context) throws AuthorizationException {
        org.apache.axis.MessageContext ctx = (org.apache.axis.MessageContext) context;
        SecurityManager manager = SecurityManager.getManager(ctx);
        Subject systemSubject = null;
        try {
            systemSubject = manager.getSystemSubject();
        } catch (org.globus.wsrf.security.SecurityException e) {
            log.warn("No System credentials were found", e);
            throw new AuthorizationException("No System credentials were found: " + e.getMessage() , e);
        }
        GlobusGSSCredentialImpl credential = (GlobusGSSCredentialImpl)JaasGssUtil.getCredential(systemSubject);
        return credential;
    }
    
    // ---------------------------------------------------------------------
    String retrieveDN(GlobusGSSCredentialImpl credential) {
        return credential.getGlobusCredential().getIdentity();
    }
}