/*
 * Copyright 2002 Tryllian BV and Otto Moerbeek
 * http://www.tryllian.com
 * otto@tryllian.com
 */

package net.drijf.javaone;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.CodeSource;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.security.Policy;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;

/** 
 * This class implements an authorization policy that enables granting of 
 * authorizition based on the CA of the associated certificates and based on
 * <code>Role</code>s.
 *
 * @author Otto Moerbeek
 * @see Role
 */
public class DelegatingPolicy extends Policy {
    private File keystorePath;
    private KeyStore keyStore;
    private char[] password;
    private RoleMapping roledefs;
    
    /**
     * Construct a new <code>DelegatingPolicy</code>. Read the list of known
     * certificates from a keystore.
     *
     * @param keystorePath the path of the file containing the keystore.
     * @param password  the password associated with the keystore.
     * @param roledefs the map mapping role names to <code>Role</code>s. 
     */
    public DelegatingPolicy(File keystorePath, char[] password,
                            RoleMapping roledefs)
    {
        this.keystorePath = keystorePath;
        this.password = password;
        this.roledefs = roledefs;
        
        refresh();
    }
    
    /** 
     * Refreshes the policy definition. This is done be rereading the keystore
     * file.
     */
    public synchronized void refresh() {
        InputStream is = null;
        try {
            KeyStore ks = KeyStore.getInstance("jks");
            is = new BufferedInputStream(new FileInputStream(keystorePath));
            ks.load(is, password);
            this.keyStore = ks;
        }
        catch (Exception e) {
            throw new RuntimeException("Cannot load keystore: "
            + e.getMessage());
        }
        finally {
            if (is != null) {
                try {
                    is.close();
                }
                catch (IOException ignored) {
                }
            }
        }
    }
    
    /**
     * Get the permission collection associated with a code source.
     * 
     * The permissions associated with a code source are defined to be the
     * permissions associated with the certificates associated with a
     * codesource.
     *
     * @param cs the code source to return the permissions of.
     * @return the permissions associated with the code source.
     */    
    public PermissionCollection getPermissions(CodeSource cs) {
        return getPermissions(cs.getCertificates());
    }
    
    /**
     * Return a permission collection associated with a list of certificates.
     *
     * @param certs an array of certificates. These certificates should be
     * X509 certificates.
     * @return The permissions associated with the certificates
     * @see java.security.cert.X509Certificate
     */    
    public PermissionCollection getPermissions(Certificate[] certs) {
        PermissionCollection permissions = new Permissions();

        // If it's unsigned code, assign the null Role.
        if (certs == null) {
            addPermissions(permissions, getPermissions((String) null));
        }        
        else {
            // Split up the certificate array into seperate chains
            List chains = splitCertificateArray(certs);

            // For each chain, validate the chain and retrieve the
            // associated permissions. Add these permissions to the set
            // of permissions to grant.
            for (Iterator i = chains.iterator(); i.hasNext(); ) {
                List aChain = (List) i.next();
                //System.out.println(chainToString(aChain));
                try {
                    String alias = verifyChain(aChain);
                    if (alias != null) {
                        PermissionCollection p = getPermissions(alias);
                        addPermissions(permissions, p);
                    }
                }
                catch (GeneralSecurityException gse) {
                    // If a single chain fails, the other chains  might still 
                    // be valid. So we continue the loop.
                    System.out.println(gse.getMessage());
                }
            }
        }
        
        permissions.setReadOnly();
        return permissions;
    }
    

    /**
     * Return a string representation of a certificate chain.
     * @param chain the certificate chain.
     * @return a string represenation of the chain.
     */
    private static String chainToString(List chain) {
        StringBuffer ret = new StringBuffer();
        ret.append('[');
        int count = 0;
        for (Iterator i = chain.iterator(); i.hasNext(); ) {
            X509Certificate cert = (X509Certificate) i.next();
            ret.append(cert.getSubjectDN().toString());
            if (count++ < chain.size() - 1) {
                ret.append("; ");
            }
        }
        ret.append(']');
        return ret.toString();
    }


    /** 
     * Return the permissions associated with a role.
     *
     * @param alias the name of the role.
     * @return a permission collection containing the permissions. Never
     * <code>null</code>.
     */    
    public PermissionCollection getPermissions(String alias) {
        Role role = roledefs.getRole(alias);
        if (role == null) {
            role = RoleImpl.NOTHING;
        }
        return role.getPermissions();
    }
    
    /**
     * Add permissions to a permission collection.
     *
     * @param set1 the collection to add to.
     * @param set2 the permissions to be added.
     */    
    public static void addPermissions(PermissionCollection set1,
        PermissionCollection set2)
    {
        if (set2 == null) {
            return;
        }
        
        Enumeration enum = set2.elements();
        while (enum.hasMoreElements()) {
            set1.add((Permission) enum.nextElement());
        }
    }

    /**
     * Split up a certificate array returned by
     * CodeSource.getCertficates into seperate chains.
     * @param certs the array of certificates.
     * @return a list of lists containing the chains.
     */
    public static List splitCertificateArray(Certificate[] certs) {

        List ret = new ArrayList();
        List chain = new ArrayList();
        ret.add(chain);
        chain.add(certs[0]);

        for (int i = 1; i < certs.length; i++) {

            X509Certificate cert1 = (X509Certificate) certs[i - 1];
            X509Certificate cert2 = (X509Certificate) certs[i];

            if (cert1.getIssuerDN().equals(cert2.getSubjectDN())
                && Arrays.equals(cert1.getIssuerUniqueID(),
                                 cert2.getSubjectUniqueID())) {
            }
            else {
                chain = new ArrayList();
                ret.add(chain);
            }
            chain.add(cert2);
        }
        return ret;
    }

    /**
     * Do a quite strict verification of a chain of certificates. The
     * standard verify code allows for expired certificates. This
     * method does not allow that.
     *
     * @param chain the certificate chain to verify.
     * @return the alias of the first know certificate in the chain,
     * <code>null</code> if no alias was found in the keystore.
     * @throws GeneralSecurityException if the chain fails verification
     */
    public String verifyChain(List chain)
        throws GeneralSecurityException
    {
        String firstKnownCert = null;
        
        for (int i = 0; i < chain.size(); i++) {

            // Get the i'th certificate from the chain.
            X509Certificate cert = (X509Certificate) chain.get(i);

            // Remember the first alias found in the keystore.
            if (firstKnownCert == null) {
                firstKnownCert = keyStore.getCertificateAlias(cert);
            }
            
            // Sun does not check vaidity, I do.
            cert.checkValidity();

            // Get the signer's certificate. For the last certificate in the
            // chain, it should be the certificate itself.
            X509Certificate issuerCert = (X509Certificate) chain.get(
                i + 1 < chain.size() ? i + 1 : i);

            // Check the subject - issuer relation.
            if (!cert.getIssuerDN().equals(issuerCert.getSubjectDN())
                || !Arrays.equals(cert.getIssuerUniqueID(),
                                  issuerCert.getSubjectUniqueID())) {
                throw new CertificateException("Chain broken");
            }
            
            // Verify not needed, already done by jar loader.
            //cert.verify(issuerCert.getPublicKey());

        }
        return firstKnownCert;
    }
}


