/*
 ************************************************************************
 *
 * Copyright (c) 2003-2004, C&C Research Laboratories, NEC Europe Ltd.
 *
 * Copyright in this software belongs to C&C Research Laboratories,
 * Rathausallee 10, 53757 Sankt Augustin, Germany.
 *
 * This software may not be used, sold, licensed, transferred, copied
 * or reproduced in whole or in part in any manner or form or in or
 * on any media by any person other than in accordance with the terms
 * of the Licence Agreement supplied with the software, or otherwise
 * without the prior written consent of the copyright owners.
 *
 * This software is distributed WITHOUT ANY WARRANTY, without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 * PURPOSE, except where stated in the Licence Agreement supplied with
 * the software.
 *
 *  Created By :           G.A. Kohring
 *  Created for Project :  GEMSS (IST-2001-37153)
 *
 ************************************************************************
 */

package de.nece.ccrle.sandbox;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.AllPermission;
import java.security.CodeSource;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Permission;
import java.security.Permissions;
import java.security.PermissionCollection;
import java.security.Policy;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.security.ProtectionDomain;
import javax.security.auth.Subject;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.PKIXCertPathBuilderResult;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Vector;
import java.util.logging.Logger;


/**
 * A class for representing a sandbox security policy in the runtime system.
 * The permission file is read from the following locations:
 * <pre>
 * ${gemss.home}/components/sandbox/sandbox.policy,  and
 * ${gemss.home}/config/sandbox.policy,
 * </pre> 
 * or from a file specified by the system variable:
 * <pre>
 * -Dgemss.sandbox.policy=my_policy.URL
 * </pre> 
 * These files are cummulative, i.e., any permissions a code receives 
 * is the union of permissions in each file which exists. 
 * Using the system variable
 * <pre>
 * -Dgemss.sandbox.policy==my_policy.URL
 * </pre> 
 * results in the specified policy being used exclusively.
 * <p>
 * Note, all of these policies are in addition to any system wide policies.
 *
 * @author Greg Kohring
 */
final public class SandboxPolicy extends Policy {

    private static final String POLICY_1;
    private static final String POLICY_2;
    private static final String SYS_PROP = "gemss.sandbox.policy";

    static {
        POLICY_1 = System.getProperty( "gemss.home" ) +
                                        "/security/sandbox/sandbox.policy";
        POLICY_2 = System.getProperty( "gemss.home" ) + 
                                        "/config/sandbox.policy";
    }

    private Policy systemPolicy;
    private ProtectionDomain sandboxDomain;
    private AccessControlContext sandboxContext;
    private TrustManager trustManager;
    private Vector grantEntries;

    private static Logger logger =
                       Logger.getLogger( SandboxPolicy.class.getName() );


    /**
     * Creates a new sandbox using the specified trust manager for assigning
     * trust to the code source.
     *
     * @param trustManager a <code>TrustManager</code> for assigning trust
     *      levels to code.
     * @throws GeneralSecurityException if a an error occurs while
     *      initializing the policy.
     */
    public SandboxPolicy(TrustManager trustManager)
                        throws GeneralSecurityException {

        // Save the trust manager

        this.trustManager = trustManager;

        // Remember the current context

        final Class thisClass = this.getClass(); 
        AccessController.doPrivileged( 
            new PrivilegedAction() { 
                public Object run() { 
                    sandboxDomain  = thisClass.getProtectionDomain(); 
                    sandboxContext = AccessController.getContext();
                    systemPolicy   = Policy.getPolicy();
                    return null;
                } 
            }
        );

        // Initialize any variables

        grantEntries = new Vector();

        // Read the policy files

        readPolicyFiles();
    }
   
    /**
     * Evaluates the global policy and returns a PermissionCollection
     * object specifying the set of permissions allowed for code from the
     * specified code source.
     * <p>
     * Unsigned code recieves the standard system permissions as defined
     * in the <code>java.policy</code> file, unless the user has specified
     * another policy file on the command line. Signed code recieves
     * permissions from the application specific policy files as
     * defined above.
     *
     * @param codesource the <code>CodeSource</code> associated with the 
     *      caller. This encapsulates the original location of the code 
     *      (where the code came from) and the public key(s) of its signer.
     * @return the set of permissions allowed for code from 
     *      <code>codesource</code> according to the policy.
     */
    public PermissionCollection getPermissions( CodeSource codesource ){

        Permissions permissions = new Permissions();
        PermissionCollection systemPC = null;
        PermissionCollection sandboxPC = null;
        Enumeration perms = null;

        sandboxPC = assignPermissions( codesource );

        perms = sandboxPC.elements();

        while ( perms.hasMoreElements() ) {
            permissions.add( (Permission) perms.nextElement() );
        }

        systemPC = systemPolicy.getPermissions( codesource );

        perms = systemPC.elements();

        while ( perms.hasMoreElements() ) {
            permissions.add( (Permission) perms.nextElement() );
        }

        return permissions;
    }

    /**
     * Evaluates the global policy for the permissions granted to
     * the ProtectionDomain and tests whether the permission is
     * granted.
     * <p>
     * The protectin domain containing the sandbox is assumed to be
     * completely trusted and is granted all permissions. For all other
     * domains, the default system policy object is first consulted for the
     * requested permission. If this is not granted, then the current policy
     * object is consulted. This allows the user to override the sandbox
     * policies by entering a policy file on the command line.
     *
     * @param domain the <code>ProtectionDomain</code> to test
     * @param permission the <code>Permission</code> object to be tested 
     *      for implication.
     *
     * @return <code>true</code> if <em>permission</em> is a proper subset 
     *      of a permission granted to this ProtectionDomain.
     */
    public boolean implies( ProtectionDomain domain, Permission permission ) {

        if ( domain == sandboxDomain ){
            return true;
        }

        return super.implies( domain, permission );
    }

                                                                                
    /**
     * Refreshes/reloads the policy configuration.
     */
    public void refresh(){

        systemPolicy.refresh();
        grantEntries = new Vector();
        readPolicyFiles();
    }

    /**
     * Assign permissions to the codesource. This method loops over all the
     * known Grant Entries searching for permissions which can be assign to
     * the specified codesource.
     *
     * @param codesource the <code>CodeSource</code> to which permissions are
     *      are to be defined.
     * @return a <code>PermissionCollection</code> object containing all the
     *      permissions to be assigned to the specified codesource.
     */
    private PermissionCollection assignPermissions( CodeSource codesource ){

        List results = null;
        Certificate[] certs = codesource.getCertificates();

        if ( certs != null ) {
            try {
                results = trustManager.checkCodeSigningTrusted( certs );
            } catch ( CertificateException ce ){
                System.err.println( "Error at " + codesource.getLocation() +
                                        ": " + ce.getMessage() );
            }
        }

        Permissions myPermissions = new Permissions();

        // loop over all grant entries
        for( Iterator geIt = grantEntries.iterator(); geIt.hasNext(); ){

            boolean grantPermissions = false;

            PolicyParser.GrantEntry grantEntry = (PolicyParser.GrantEntry)
                                                    geIt.next();

            // If the signedBy entry is present then it must be respected
            if ( grantEntry.signedBy != null ){

                grantPermissions = false;

                // find all the aliases for this SignedBy
                String alias[] = grantEntry.signedBy.split( "," );

                // According to the specification the code must be signed
                // by all the aliases or it fails
                for ( int a = 0; a < alias.length; a++ ){
                    if ( isSignedBy( results, alias[a] ) ){
                        grantPermissions = true;
                    } else {
                        grantPermissions = false;
                        break;
                    }
                }
                if ( !grantPermissions ) continue;
            }

            // If the trustedBy entry is present then it must be respected
            if ( grantEntry.trustedBy != null ){

                grantPermissions = false;

                // find all the aliases for this TrustedBy
                String alias[] = grantEntry.trustedBy.split( "," );


                // Grant if the code is signed by any of the alias.
                for ( int a = 0; a < alias.length; a++ ){
                    if ( isTrustedBy( results, alias[a] ) ){
                        grantPermissions = true;
                        break;
                    } 
                }
                if ( !grantPermissions ) continue;
            }

            // If the codeBase entry is present then it must be respected
            if ( grantEntry.codeBase != null ){
                grantPermissions = false;

                // Create a new codeSource using the grant entries code base

                URL codeBase = null;
                try {
                    codeBase = new URL( grantEntry.codeBase );
                } catch ( MalformedURLException murle ){
                    System.err.println( "Malformed URL: " + codeBase 
                                 + " from Policy file." );
                }

                CodeSource codeBaseCodeSource = 
                    new CodeSource( codeBase, codesource.getCertificates() );

                if ( codeBaseCodeSource.implies( codesource ) ){
                    grantPermissions = true;
                }

                if ( !grantPermissions ) continue;
            } 

            // If the principals entry is present then it must be respected
            if ( ( grantEntry.principals != null ) && 
                        ( grantEntry.principals.size() > 0 ) ){
               
                grantPermissions = false;

                // Get the subject for the current context

                final AccessControlContext currentContext =
                                        AccessController.getContext();

                Subject subject = (Subject) AccessController.doPrivileged( 
                    new PrivilegedAction() { 
                        public Object run() { 
                            return Subject.getSubject( currentContext );
                        } 
                    }, sandboxContext );


                if ( subject != null ){

                    // loop over all the principal entries

                    for ( Iterator pit = grantEntry.principals.iterator(); 
                                                    pit.hasNext();  ){

                        PolicyParser.PrincipalEntry pe =
                                (PolicyParser.PrincipalEntry) pit.next();

                        try {
                            // get the principals corresponding to the
                            // class specified in principal entry

                            Class principalClass = 
                                    Class.forName( pe.principalClass );
                            Set principals = subject.getPrincipals( 
                                                        principalClass );

                            // loop over the set of principals for this
                            // thread
                            for ( Iterator apit = principals.iterator();
                                                apit.hasNext(); ){
                                Principal principal = (Principal) apit.next();
                                String name = principal.getName();
                                if ( name.equals( pe.principalName ) ){
                                    grantPermissions = true;
                                    break;
                                }
                            }
                        } catch ( ClassNotFoundException cnfe ){
                            System.err.println( "Class not found: " + 
                                                    cnfe.getMessage() );
                        }
                    }

                }

                if ( !grantPermissions ) continue;

            }

            // If we have gotten this far then we can grant permissions
            // the the codesource

            Class permissionClass = null;

            // Loop over all permission entries
            for ( Iterator pit = grantEntry.permissionEntries.iterator(); 
                                pit.hasNext(); ) {
                PolicyParser.PermissionEntry pe =
                                (PolicyParser.PermissionEntry) pit.next();

                try {
                        permissionClass = Class.forName( pe.permission );
                } catch ( ClassNotFoundException cnfe ){
                        System.err.println( "No such permission: " +
                                pe.permission );
                        continue;
                }

                    // check for signed permissions
                    if ( pe.signedBy != null ){
                        final Class fPermissionClass = permissionClass;
                        CodeSource pcs = 
                            (CodeSource) AccessController.doPrivileged( 
                            new PrivilegedAction() { 
                                public Object run() { 
                                    return fPermissionClass.
                                                    getProtectionDomain().
                                                        getCodeSource();
                                } 
                            }, sandboxContext );
                        Certificate[] pcCerts = pcs.getCertificates();
                        try {
                            List peResults = trustManager.
                                            checkCodeSigningTrusted( pcCerts );
                            String alias[] = pe.signedBy.split( "," );

                            boolean wasSigned = false;
                            for ( int a = 0; a < alias.length; a++ ){
                                if ( isSignedBy( peResults, alias[a] ) ){
                                    wasSigned = true;
                                } else {
                                    wasSigned = false;
                                    break;
                                }
                            }
                            if ( !wasSigned ) continue;
                        } catch ( CertificateException ce ){
                            System.err.println( "Error at " + pcs.getLocation()
                                            + ": " + ce.getMessage() );
                            continue;
                        }
                    }

                    // check for trusted permissions
                    if ( pe.trustedBy != null ){
                        final Class fPermissionClass = permissionClass;
                        CodeSource pcs = 
                            (CodeSource) AccessController.doPrivileged( 
                            new PrivilegedAction() { 
                                public Object run() { 
                                    return fPermissionClass.
                                                    getProtectionDomain().
                                                        getCodeSource();
                                } 
                            }, sandboxContext );
                        Certificate[] pcCerts = pcs.getCertificates();
                        try {
                            List peResults = trustManager.
                                            checkCodeSigningTrusted( pcCerts );
                            String alias[] = pe.trustedBy.split( "," );

                            boolean wasTrusted = false;
                            for ( int a = 0; a < alias.length; a++ ){
                                if ( isTrustedBy( peResults, alias[a] ) ){
                                    wasTrusted = true;
                                    break;
                                } 
                            }
                            if ( !wasTrusted ) continue;
                        } catch ( CertificateException ce ){
                            System.err.println( "Error at " + pcs.getLocation()
                                            + ": " + ce.getMessage() );
                            continue;
                        }
                    }


                    // Create a new permission object 

                    // 1. Create a constructor with the specified arguments
                   
                    Class argTypes[] = null;
                    String argVals[] = null;
                    if ( ( pe.name != null ) && (pe.action != null ) ){
                        argTypes = new Class[]{ String.class, String.class };
                        argVals = new String[]{ pe.name, pe.action };
                    } else if ( pe.name != null ) {
                        argTypes = new Class[]{String.class};
                        argVals = new String[]{pe.name};
                    }

                    // 2. Create the new permission

                    try{
                        Permission permission = null;

                        if ( argTypes == null ){

                            permission = (Permission) 
                                            permissionClass.newInstance();

                        } else {

                            Constructor constuctor = 
                                permissionClass.getConstructor( argTypes );
                            
                            permission = (Permission) 
                                        constuctor.newInstance( argVals );
                        }

                        myPermissions.add( permission );

                    } catch ( InstantiationException ie ){
                        System.err.println( "Not found: " +
                                pe.permission + " " + ie.getMessage() );
                    } catch ( NoSuchMethodException nsme ){
                        System.err.println( "Not found: " +
                                pe.permission + " " + nsme.getMessage() );
                    } catch ( IllegalAccessException iae ){
                        System.err.println( "Not found: " +
                                pe.permission + " " + iae.getMessage() );
                    } catch ( IllegalArgumentException iae2 ){
                        System.err.println( "Not found: " +
                                pe.permission + " " + iae2.getMessage() );
                    } catch ( InvocationTargetException ite ){
                        System.err.println( "Not found: " +
                                pe.permission + " " + ite.getMessage() );
                    }
                    
            }

        }
    
        return myPermissions;
    }

    // Checks whether any of the results in the list were obtained  
    // with the specified alias as the signing certificate
    private boolean isSignedBy( List results, String alias ){

        if ( results == null ) return false;

        boolean signedBy = false;
        PKIXCertPathBuilderResult builderResults = null;


        // loop over the list of results looking for the signing
        // certificate
        for ( Iterator rit = results.iterator(); rit.hasNext(); ){

            builderResults = (PKIXCertPathBuilderResult) rit.next();
            X509Certificate signer = (X509Certificate)
                    builderResults.getCertPath().getCertificates().get(0);

            if ( trustManager.knownAs( signer, alias ) ) {
                signedBy = true;
                break;
            }
        }

        return signedBy;
    }

    // Checks whether any of the results in the list were obtained  
    // with the specified alias as the trust anchor
    private boolean isTrustedBy( List results, String alias ){

        if ( results == null ) return false;

        boolean trustedBy = false;
        PKIXCertPathBuilderResult builderResults = null;


        // loop over the list of results looking for the signing
        // certificate
        for ( Iterator rit = results.iterator(); rit.hasNext(); ){

            builderResults = (PKIXCertPathBuilderResult) rit.next();
            X509Certificate trusted = (X509Certificate)
                            builderResults.getTrustAnchor().getTrustedCert();

            if ( trustManager.knownAs( trusted, alias ) ) {
                trustedBy = true;
                break;
            }
        }

        return trustedBy;
    }

    // Read the policy files
    private void readPolicyFiles() {

        File policyFile = null;
        boolean loaded = false;

        // Check for policy files on the command line

        String comLinePolicy = System.getProperty( SYS_PROP );

        if ( comLinePolicy != null ){

            boolean authoritive = false;

            if ( comLinePolicy.startsWith( "=" ) ){
                comLinePolicy = comLinePolicy.substring( 1 );
                authoritive = true;
            }

            policyFile = new File( comLinePolicy );

            try{
                loaded = loadPolicy( policyFile.toURI().toURL() );
            } catch ( MalformedURLException murle ){
                logger.warning( "Could not parse policy file: " 
                            + policyFile + " : " + murle.getMessage() );
            }

            if ( authoritive ){
                if ( !loaded ){
                    logger.warning( "Could not load authoritive policy file: " 
                            + policyFile );
                }
                return;
            }
        }


        // Parse the standard GEMSS policy files

        policyFile = new File( POLICY_1 );

        try{
            loaded |= loadPolicy( policyFile.toURI().toURL() );
        } catch ( MalformedURLException murle ){
            logger.warning( "Could not parse policy file: " 
                            + policyFile + " : " + murle.getMessage() );
        }

        policyFile = new File( POLICY_2 );

        try{
            loaded |= loadPolicy( policyFile.toURI().toURL() );
        } catch ( MalformedURLException murle ){
            logger.warning( "Could not parse policy file: " 
                            + policyFile + " : " + murle.getMessage() );
        }

        if ( !loaded ){
            logger.warning( "No sandbox specific policy files found. " +
                "Using system policy files only." );
        }

    }

    // load the policy file
    private boolean loadPolicy( URL policyURL ){

        PolicyParser policyParser = null;

        boolean loaded = false;

        try{

            BufferedReader br = null;

            InputStreamReader isr = new InputStreamReader( 
                                                policyURL.openStream() );

            try{
                policyParser = new PolicyParser( true );
                br = new BufferedReader( isr );
                policyParser.read( br );
                grantEntries.addAll( policyParser.getGrantEntries() );
                loaded = true;
            } catch ( PolicyParser.ParsingException pe ){
                logger.warning( "Could not parse policy file: " 
                            + policyURL + " : " + pe.getMessage() );
            } catch ( IOException ioe ){
                logger.warning( "While parsing policy: " 
                            + policyURL + " : " + ioe.getMessage() );
            } finally {
                try {
                    if ( br != null ) br.close();
                } catch ( IOException ioe ){
                    logger.warning( "While parsing policy: " 
                            + policyURL + " : " + ioe.getMessage() );
                }
            }

            // Check the keystore location entry
            if ( policyParser != null ){

                String keyStoreURLstring = policyParser.getKeyStoreUrl();
                String keyStoreType = policyParser.getKeyStoreType();

                if ( keyStoreURLstring != null ){
                    InputStream is = null;
                    try{
                        URL keyStoreURL = new URL( keyStoreURLstring );
                        KeyStore trustStore = null;
                        if ( keyStoreType != null ){
                            trustStore = KeyStore.getInstance( keyStoreType );
                        } else {
                            trustStore = KeyStore.getInstance( "JKS" );
                        }
                        is = keyStoreURL.openStream();
                        trustStore.load( is , null );
                        trustManager.addTrustedCerts( trustStore );
                    } catch ( MalformedURLException murle ){
                        logger.warning( "While parsing policy: " 
                            + policyURL + " : " + murle.getMessage() );
                    } catch ( NoSuchAlgorithmException nsae ){
                        logger.warning( "While parsing policy: " 
                            + policyURL + " : " + nsae.getMessage() );
                    } catch ( CertificateException ce ){
                        logger.warning( "While parsing policy: " 
                            + policyURL + " : " + ce.getMessage() );
                    } catch ( KeyStoreException kse ){
                        logger.warning( "While parsing policy: " 
                            + policyURL + " : " + kse.getMessage() );
                    } catch ( IOException ioe ){
                        logger.warning( "While parsing policy: " 
                            + policyURL + " : " + ioe.getMessage() );
                    } finally {
                        try {
                            if ( is != null ) is.close();
                        } catch ( IOException ioe ){
                            logger.warning( "While parsing policy: " 
                                    + policyURL + " : " + ioe.getMessage() );
                        }
                    }
                }
            }

        } catch ( IOException ioe ){
            // We can ignore this error, since it simply means the file
            // is not present.
        }

        return loaded;
    }

}
