/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.sun.jini.tool;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.URL;
import java.security.AccessController;
import java.security.CodeSource;
import java.security.Permission;
import java.security.Policy;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.security.ProtectionDomain;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.jini.security.policy.DynamicPolicy;
import net.jini.security.policy.DynamicPolicyProvider;
import net.jini.security.policy.PolicyInitializationException;

/**
 * Defines a {@link DynamicPolicy} that logs information about missing
 * permissions, and optionally grants all permissions, which is <b>FOR
 * DEBUGGING ONLY</b>. Do not use this security policy provider to grant
 * all permissions in a production environment. <p>
 *
 * This class is intended to simplify the process of deciding what security
 * permissions to grant to run an application.  While it is generally
 * acceptable to grant all permissions to local, trusted code, downloaded
 * code should typically be granted the least permission possible. <p>
 *
 * The usual approach to choosing which permissions to grant is to start by
 * running the application with a security policy file that grants all
 * permissions to local, trusted code.  When the application fails with an
 * exception message that identifies a missing permission, add that
 * permission to the security policy file, and repeat the process. Although
 * straight forward, this process can be time consuming if the application
 * requires many permission grants. <p>
 *
 * Another approach is to set the value of the
 * <code>"java.security.debug"</code> system property to
 * <code>"access,failure"</code>, which produces debugging output that
 * describes permission grants and failures. Unfortunately, this approach
 * produces voluminous output, making it difficult to determine which
 * permission grants are needed. <p>
 *
 * This security policy provider permits another, hopefully more
 * convenient, approach. When this class is specified as the security
 * policy provider, and granting all permissions is enabled, it uses the
 * standard dynamic security policy to determine what permissions are
 * granted. If a permission is not granted by the standard policy, though,
 * then rather than denying permission, this class logs the missing
 * permission in the form required by the security policy file, and grants
 * the permission, allowing the program to continue. In this way,
 * developers can determine the complete set of security permissions
 * required by the application. <p>
 *
 * Note that the information printed by this security policy provider may
 * not be in the form you wish to use in your policy file. In particular,
 * using system property substitutions and <code>KeyStore</code> aliases
 * may produce a more portable file than one containing the exact entries
 * logged. Note, too, that the information printed for
 * <code>signedBy</code> fields specifies the principal name for
 * <code>X.509</code> certificates, rather than the <code>KeyStore</code>
 * alias, which is not a valid security policy file format. <p>
 *
 * Using this security policy provider without granting all permissions is
 * also useful since it prints information about security exceptions that
 * were caught, but that might have an affect on program behavior. <p>
 *
 * This class uses uses the {@link Logger} named
 * <code>net.jini.security.policy</code> to log information at the following
 * levels: <ul>
 *
 * <li> {@link Level#WARNING WARNING} - Permissions that were needed but not
 * granted by the policy file.
 *
 * <li> {@link Level#FINE FINE} - Also include stack traces.
 *
 * <li> {@link Level#FINER FINER} - All permissions granted, with stack traces
 * for ones not granted by the policy file, and dynamic grants.
 *
 * <li> {@link Level#FINEST FINEST} - All permissions granted, with all stack
 * traces, and dynamic grants. </ul>
 *
 * To use this security policy provider, do the following: <ul>
 *
 * <li> Copy the <code>jsk-policy.jar</code> file from the <code>lib-ext</code>
 * subdirectory of the Apache River release
 * installation to the extensions directory of the Java(TM) 2 SDK (or JRE)
 * installation, and copy the <code>jsk-debug-policy.jar</code> file
 * from the <code>lib</code> subdirectory of the Apache River release installation to
 * the extensions directory of the Java 2 SDK (or JRE) installation.
 *
 * <li> Specify this class as the security policy provider. Create a copy of
 * the file <code>jre/lib/security/security/java.security</code>, modify the
 * file to contain the line:
 *
 * <blockquote>
 * <pre>
 * policy.provider=com.sun.jini.tool.DebugDynamicPolicyProvider
 * </pre>
 * </blockquote>
 *
 * and then specify this new file as the value of the
 * <code>java.security.properties</code> system property.  
 *
 * <li> Specify whether all permissions should be granted by setting the
 * <code>com.sun.jini.tool.DebugDynamicPolicyProvider.grantAll</code> security
 * property to <code>true</code> by adding the following line to the security
 * properties file:
 * 
 * <blockquote>
 * <pre>
 * com.sun.jini.tool.DebugDynamicPolicyProvider.grantAll=true
 * </pre>
 * </blockquote> </ul> <p>
 *
 * Granting all permissions is disabled by default. <p>
 *
 * Make sure to specify a security manager, either by setting the
 * <code>java.security.manager</code> system property, or putting the following
 * code in the main method of the application:
 *
 * <blockquote>
 * <pre>
 * if (System.getSecurityManager() == null) {
 *     System.setSecurityManager(new SecurityManager());
 * }
 * </pre>
 * </blockquote>
 *
 * <p>This provider can be used in conjunction with the provider
 * <code>com.sun.jini.start.AggregatePolicyProvider</code> by setting the
 * <code>com.sun.jini.start.AggregatePolicyProvider.mainPolicyClass</code> 
 * system property to the fully qualified name of this class.  If this
 * provider is used with the <code>AggregatePolicyProvider</code>, then the
 * JAR file <code>jsk-debug-policy.jar</code> needs to be in the
 * application's class path, and this class needs to be granted all
 * permissions. 
 *
 *
 * 
 **/
public class DebugDynamicPolicyProvider extends DynamicPolicyProvider {

    /* Logger to use */
    private static final Logger logger =
	Logger.getLogger("net.jini.security.policy");

    /* If true, always grant permission */
    private static boolean grantAll =
	((Boolean) AccessController.doPrivileged(
	    new PrivilegedAction() {
		public Object run() {
		    return Boolean.valueOf(
			Security.getProperty(
			    "com.sun.jini.tool." +
			    "DebugDynamicPolicyProvider.grantAll"));
		}
	    })).booleanValue();

    /* Cache of permission requests already made */
    private static final Set requests = new HashSet();

    /** The empty codesource. */
    private static final CodeSource emptyCS =
	new CodeSource(null, (Certificate[]) null);

    /**
     * Creates an instance of this class that wraps a default underlying
     * policy, as specified by {@link
     * DynamicPolicyProvider#DynamicPolicyProvider() DynamicPolicyProvider()}.
     *
     * @throws PolicyInitializationException if unable to construct the base
     *	       policy
     * @throws SecurityException if there is a security manager and the calling
     *	       context does not have adequate permissions to read the <code>
     *	       net.jini.security.policy.DynamicPolicyProvider.basePolicyClass
     *	       </code> security property, or if the calling context does not
     *	       have adequate permissions to access the base policy class
     */
    public DebugDynamicPolicyProvider() throws PolicyInitializationException { }

    /**
     * Creates an instance of this class that wraps around the given
     * non-<code>null</code> base policy object.
     *
     * @param basePolicy base policy object containing information about
     *        non-dynamic grants
     * @throws NullPointerException if <code>basePolicy</code> is
     * 	       <code>null</code>
     */
    public DebugDynamicPolicyProvider(Policy basePolicy) {
	super(basePolicy);
    }

    /** Log calls. */
    public void grant(Class cl,
		      Principal[] principals,
		      Permission[] permissions)
    {
	try {
	    super.grant(cl, principals, permissions);
	    if (permissions == null
		|| permissions.length == 0
		|| !logger.isLoggable(Level.FINER))
	    {
		return;
	    }
	    Request req = new Request(cl, principals, permissions);
	    if (cl == null) {
		logger.log(Level.FINER,
			   "Granting permissions for all classes:\n{0}",
			   req.toString());
	    } else {
		logger.log(Level.FINER,
			   "Granting permissions for {0}:\n{1}",
			   new Object[] { cl, req.toString() });
	    }
	} catch (SecurityException e) {
	    if (logger.isLoggable(Level.FINE)) {
		logger.log(Level.FINE, "Granting permissions failed", e);
	    }
	    throw e;
	}
    }

    /** Always returns true, but logs unique requests */
    public boolean implies(ProtectionDomain pd, Permission perm) {
	boolean implies = super.implies(pd, perm);
	boolean result = implies ? true : grantAll;
	if (!(logger.isLoggable(Level.FINER)
	      || (!implies && logger.isLoggable(Level.WARNING))))
	{
	    return result;
	}
	Request request = new Request(pd, perm);
	synchronized (requests) {
	    if (requests.contains(request)) {
		return result;
	    }
	    requests.add(request);
	}
	String stackTrace = null;
	if (logger.isLoggable(Level.FINEST)
	    || (!implies && logger.isLoggable(Level.FINE)))
	{
	    StringWriter sw = new StringWriter();
	    PrintWriter pw = new PrintWriter(sw);
	    new Exception("Stack trace:").printStackTrace(pw);
	    pw.close();
	    stackTrace = sw.toString();
	}
	if (implies) {
	    if (logger.isLoggable(Level.FINEST)) {
		logger.log(Level.FINEST, "Permission granted:\n{0}\n{1}",
			   new Object[] { request.toString(), stackTrace });
	    } else {
		logger.log(Level.FINER, "Permission granted:\n{0}",
			   request.toString());
	    }
	} else if (logger.isLoggable(Level.FINE)) {
	    logger.log(Level.WARNING,
		       (grantAll
			? "Permission not granted by base policy:\n{0}\n{1}"
			: "Permission not granted:\n{0}\n{1}"),
		       new Object[] { request.toString(), stackTrace });
	} else {
	    logger.log(Level.WARNING,
		       (grantAll
			? "Permission not granted by base policy:\n{0}"
			: "Permission not granted:\n{0}"),
		       request.toString());
	}   
	return result;
    }

    /** Returns the name of the certificate. */
    private static String getCertName(Certificate cert) {
	if (cert instanceof X509Certificate) {
	    return ((X509Certificate) cert).getSubjectDN().getName();
	} else {
	    return cert.toString();
	}
    }

    /**
     * Returns a quoted version of the argument, such that it would result in
     * the argument if read from a file with the standard String syntax.
     */
    private static String quoteString(String s) {
	if (s == null) {
	    return "";
	}
	int len = s.length();
	StringBuffer buf = new StringBuffer(len + 2);
	buf.append('"');
	for (int off = 0; off < len; ) {
	    int quote = s.indexOf('"', off);
	    int slash = s.indexOf('\\', off);
	    if (quote >= 0 && (slash < 0 || slash > quote)) {
		buf.append(s.substring(off, quote));
		buf.append("\\\"");
		off = quote + 1;
	    } else if (slash >= 0) {
		buf.append(s.substring(off, slash));
		buf.append("\\\\");
		off = slash + 1;
	    } else {
		buf.append(s.substring(off));
		break;
	    }
	}
	buf.append('"');
	return buf.toString();
    }

    /* Keeps track of an individual permission request */
    private static class Request {
	final CodeSource codeSource;
	final Certificate[] certs;
	final Principal[] principals;
	final Permission[] perms;

	Request(ProtectionDomain pd, Permission perm) {
	    codeSource = pd.getCodeSource();
	    certs = codeSource == null ? null : codeSource.getCertificates();
	    principals = pd.getPrincipals();
	    this.perms = new Permission[] { perm };
	}

	Request(final Class cl, Principal[] principals, Permission[] perms) {
	    codeSource = (cl == null) ? emptyCS :
		(CodeSource) AccessController.doPrivileged(
		    new PrivilegedAction() {
			public Object run() {
			    return cl.getProtectionDomain().getCodeSource();
			}
		    });
	    certs = null;
	    this.principals = principals;
	    this.perms = perms;
	}

	public boolean equals(Object o) {
	    if (o instanceof Request) {
		Request other = (Request) o;
		return (codeSource == null
			? other.codeSource == null
			: equals(codeSource.getLocation(),
				 other.codeSource.getLocation()))
		    && equals(principals, other.principals)
		    && equals(perms, other.perms);
	    } else {
		return false; 
	    }
	}

	private static boolean equals(Object x, Object y) {
	    return (x == null) ? y == null : x.equals(y);
	}

	private static boolean equals(Object[] x, Object[] y) {
	    if (x == null) {
		return y == null;
	    } else if (y == null) {
		return false;
	    } else {
		return Arrays.equals(x, y);
	    }
	}

	public int hashCode() {
	    return hash(codeSource == null ? null : codeSource.getLocation())
		^ hash(certs) ^ hash(principals) ^ hash(perms);
	}

	private static int hash(Object obj) {
	    return (obj == null) ? 0 : obj.hashCode();
	}

	private static int hash(Object[] array) {
	    int result = 0;
	    if (array != null) {
		for (int i = array.length; --i >= 0; ) {
		    result ^= hash(array[i]);
		}
	    }
	    return result;
	}

	public String toString() {
	    StringBuffer buf = new StringBuffer();
	    buf.append("grant\n");
	    if (codeSource == null) {
		buf.append("    /* bootstrap codebase */\n");
	    } else {
		URL location = codeSource.getLocation();
		if (location != null) {
		    buf.append("    codeBase ");
		    buf.append(quoteString(location.toString()));
		    buf.append('\n');
		}
		if (certs != null) {
		    for (int i = 0; i < certs.length; i++) {
			buf.append("    signedby ");
			buf.append(quoteString(getCertName(certs[i])));
			buf.append('\n');
		    }
		}
	    }
	    if (principals != null) {
		for (int i = 0; i < principals.length; i++) {
		    buf.append("    principal ");
		    buf.append(principals[i].getClass().getName());
		    buf.append(' ');
		    buf.append(quoteString(principals[i].getName()));
		    buf.append('\n');
		}
	    }
	    buf.append("{\n");
	    for (int i = 0; i < perms.length; i++) {
		Permission perm = perms[i];
		buf.append("    permission ");
		buf.append(perm.getClass().getName());
		buf.append('\n');
		buf.append("        ");
		buf.append(quoteString(perm.getName()));
		String actions = perm.getActions();
		if (actions != null && actions.length() != 0) {
		    buf.append(",\n");
		    buf.append("        ");
		    buf.append(quoteString(perm.getActions()));
		}
		buf.append(";\n");
	    }
	    buf.append("};");
	    return buf.toString();
	}
    }
}
