/*
 * 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 net.jini.security;

import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamField;
import java.io.Serializable;
import java.rmi.RemoteException;
import java.security.Permission;
import java.util.Collections;
import net.jini.core.constraint.MethodConstraints;
import net.jini.core.constraint.RemoteMethodControl;

/**
 * A <code>ProxyPreparer</code> for verifying that proxies are trusted,
 * granting them dynamic permissions, and setting their constraints, as well as
 * for creating other proxy preparer subclasses that include those
 * operations. <p>
 *
 * Applications and configurations can use this class to create proxy preparers
 * for several common cases. Some examples include creating proxy preparers
 * that: <ul>
 *
 * <li> Verify trust, grant permissions, and set new constraints, to prepare a
 * proxy received from an untrusted source.
 *
 * <li> Use the proxy's existing constraints when verifying trust in the proxy,
 * to prepare a trusted proxy received with integrity protection from a trusted
 * source that supplies constraints, confirming that the proxy's implementation
 * is trusted locally, but allowing the constraints to be downloaded from a
 * third party.
 *
 * <li> Set new constraints, to prepare a trusted proxy received with integrity
 * protection from a trusted source that is not known to supply the appropriate
 * constraints.
 *
 * <li> Grant permissions, to prepare a trusted proxy received with integrity
 * protection from a trusted source that supplies appropriate constraints, if
 * the proxy needs permission grants.
 *
 * <li> Do nothing, to use as a default when retrieving an optional
 * configuration entry, or to prepare a non-secure proxy. </ul>
 *
 * 
 * @since 2.0
 */
public class BasicProxyPreparer implements ProxyPreparer, Serializable {

    private static final long serialVersionUID = 4439691869768577046L;

    /**
     * @serialField verify boolean
     *		    Whether to verify if proxies are trusted.
     * @serialField methodConstraintsSpecified boolean
     *		    Whether to use <code>methodConstraints</code> when
     *		    verifying if proxies are trusted and for setting their
     *		    constraints.
     * @serialField methodConstraints MethodConstraints
     *		    Method constraints to use when verifying if proxies are
     *		    trusted and for setting their constraints, if
     *		    <code>methodConstraintsSpecified</code> is
     *		    <code>true</code>. Set to <code>null</code> if
     *		    <code>methodConstraintsSpecified</code> is
     *		    <code>false</code>.
     * @serialField permissions Permission[]
     *		    Permissions to grant to proxies, or an empty array if no
     *		    permissions should be granted. The value is always
     *		    non-<code>null</code>.
     */
    private static final ObjectStreamField[] serialPersistentFields = {
	new ObjectStreamField("verify", boolean.class),
	new ObjectStreamField("methodConstraintsSpecified", boolean.class),
	new ObjectStreamField("methodConstraints", MethodConstraints.class),
	/* Make sure the deserialized permissions array is not shared */
        new ObjectStreamField("permissions", Permission[].class, true)
    };

    /** Whether to verify if proxies are trusted. */
    protected final boolean verify;

    /**
     * Whether to use {@link #methodConstraints} when verifying if proxies are
     * trusted and for setting their constraints.
     */
    protected final boolean methodConstraintsSpecified;

    /**
     * Method constraints to use when verifying if proxies are trusted and for
     * setting their constraints, if {@link #methodConstraintsSpecified} is
     * <code>true</code>. Set to <code>null</code> if
     * <code>methodConstraintsSpecified</code> is <code>false</code>.
     */
    protected final MethodConstraints methodConstraints;

    /**
     * Permissions to grant to proxies, or an empty array if no permissions
     * should be granted. The value is always non-<code>null</code>.
     */
    protected final Permission[] permissions;

    /**
     * Creates a proxy preparer that specifies not to verify proxies, grant
     * them permissions, or set their constraints.
     */
    public BasicProxyPreparer() {
	verify = false;
	methodConstraintsSpecified = false;
	methodConstraints = null;
	permissions = new Permission[0];
    }

    /**
     * Creates a proxy preparer that specifies whether proxies should be
     * verified, using the constraints on the proxy by default, and specifies
     * what permissions to grant to proxies.
     *
     * @param verify whether to verify if proxies are trusted
     * @param permissions permissions to grant, or <code>null</code> if no
     *	      permissions should be granted
     * @throws NullPointerException if <code>permissions</code> is not
     *	       <code>null</code> and any of its elements are <code>null</code>
     */
    public BasicProxyPreparer(boolean verify, Permission[] permissions) {
	this.verify = verify;
	methodConstraintsSpecified = false;
	methodConstraints = null;
	this.permissions = checkPermissions(permissions);
    }

    /* Copies the argument, if needed, and checks for null elements. */
    private static Permission[] checkPermissions(Permission[] permissions) {
	if (permissions == null) {
	    return new Permission[0];
	}
	permissions = (Permission[]) permissions.clone();
	for (int i = permissions.length; --i >= 0; ) {
	    if (permissions[i] == null) {
		throw new NullPointerException("Permission cannot be null");
	    }
	}
	return permissions;
    }

    /**
     * Creates a proxy preparer that specifies whether proxies should be
     * verified, specifies permissions to grant them, and specifies what method
     * constraints to use when verifying and setting constraints.
     *
     * @param verify whether to verify if proxies are trusted
     * @param methodConstraints method constraints to use when verifying 
     *	      and setting constraints
     * @param permissions permissions to grant, or <code>null</code> if no
     *	      permissions should be granted
     * @throws NullPointerException if <code>permissions</code> is not
     *	       <code>null</code> and any of its elements are <code>null</code>
     */
    public BasicProxyPreparer(boolean verify,
			      MethodConstraints methodConstraints,
			      Permission[] permissions)
    {
	this.verify = verify;
	this.methodConstraintsSpecified = true;
	this.methodConstraints = methodConstraints;
	this.permissions = checkPermissions(permissions);
    }

    /**
     * Returns the method constraints to use when verifying and setting
     * constraints on the specified proxy. <p>
     *
     * The default implementation returns the value of {@link
     * #methodConstraints} if {@link #methodConstraintsSpecified} is
     * <code>true</code>, else returns the constraints on the specified proxy
     * if it implements {@link RemoteMethodControl}, else returns
     * <code>null</code>. <p>
     *
     * Subclasses may wish to override this method, for example, to augment the
     * existing constraints on the proxy rather than replacing them.
     *
     * @param proxy the proxy being prepared
     * @return the method constraints to use when verifying and setting
     *	       constraints on the proxy
     */
    protected MethodConstraints getMethodConstraints(Object proxy) {
	if (methodConstraintsSpecified) {
	    return methodConstraints;
	} else if (proxy instanceof RemoteMethodControl) {
	    return ((RemoteMethodControl) proxy).getConstraints();
	} else {
	    return null;
	}
    }

    /**
     * Returns the permissions to grant to proxies, or an empty array if no
     * permissions should be granted. The return value need not be newly
     * created, but cannot be <code>null</code>. <p>
     *
     * The default implementation returns the value of {@link
     * #permissions}. <p>
     *
     * Subclasses may wish to override this method, for example, to grant
     * permissions that depend on principal constraints found on the proxy.
     *
     * @param proxy the proxy being prepared
     * @return the permissions to grant to the proxy
     */
    protected Permission[] getPermissions(Object proxy) {
	return permissions;
    }

    /**
     * Performs operations on a proxy to prepare it for use, returning the
     * prepared proxy, which may or may not be the argument itself. <p>
     *
     * The default implementation provides the following behavior. If
     * <code>proxy</code> is <code>null</code>, throws a {@link
     * NullPointerException}. Otherwise, calls {@link #verify(Object) verify}
     * with <code>proxy</code>. If the <code>verify</code> call succeeds, calls
     * {@link #grant grant} with <code>proxy</code>. If the <code>grant</code>
     * call succeeds, returns the result of calling {@link #setConstraints
     * setConstraints} with <code>proxy</code>. <p>
     *
     * Subclasses may wish to override this method, for example, to perform
     * additional operations, typically calling the default implementation via
     * <code>super</code>.
     *
     * @param proxy the proxy to prepare
     * @return the prepared proxy
     * @throws NullPointerException if <code>proxy</code> is <code>null</code>
     * @throws RemoteException if a communication-related exception occurs
     * @throws SecurityException if a security exception occurs
     * @see #verify(Object) verify
     * @see #grant grant
     * @see #setConstraints setConstraints
     */
    public Object prepareProxy(Object proxy) throws RemoteException {
	if (proxy == null) {
	    throw new NullPointerException("Proxy cannot be null");
	}
	verify(proxy);
	grant(proxy);
	return setConstraints(proxy);
    }

    /**
     * Verifies that the proxy is trusted. Called by the default implementation
     * of {@link #prepareProxy prepareProxy}. <p>
     *
     * The default implementation provides the following behavior. If
     * <code>proxy</code> is <code>null</code>, throws a {@link
     * NullPointerException}. Otherwise, if {@link #verify} is
     * <code>true</code>, calls {@link Security#verifyObjectTrust
     * Security.verifyObjectTrust}, with <code>proxy</code>, <code>null</code>
     * for the class loader, and, for the context, a collection containing the
     * result of calling {@link #getMethodConstraints getMethodConstraints}
     * with <code>proxy</code>, or an empty collection if the constraints are
     * <code>null</code>. <p>
     *
     * Subclasses may wish to override this method, for example, to specify a
     * different class loader or context when verifying the proxy.
     *
     * @param proxy the proxy to verify
     * @throws NullPointerException if <code>proxy</code> is <code>null</code>
     * @throws RemoteException if a communication-related exception occurs
     * @throws SecurityException if verifying that the proxy is trusted fails
     * @see #prepareProxy prepareProxy
     * @see #getMethodConstraints getMethodConstraints
     * @see Security#verifyObjectTrust Security.verifyObjectTrust
     */
    protected void verify(Object proxy) throws RemoteException {
	if (proxy == null) {
	    throw new NullPointerException("Proxy cannot be null");
	} else if (verify) {
	    MethodConstraints mc = getMethodConstraints(proxy);
	    Security.verifyObjectTrust(proxy, null,
				       (mc == null
					? Collections.EMPTY_SET
					: Collections.singleton(mc)));
	}
    }

    /**
     * Grants permissions to the proxy. Called by the default implementation of
     * {@link #prepareProxy prepareProxy} unless {@link #verify(Object) verify}
     * throws an exception. <p>
     *
     * The default implementation provides the following behavior. If
     * <code>proxy</code> is <code>null</code>, throws a {@link
     * NullPointerException}. Otherwise, calls {@link #getPermissions
     * getPermissions} with <code>proxy</code> to determine what permissions
     * should be granted. If the permissions are not empty, calls {@link
     * Security#grant(Class,Permission[]) Security.grant}, with the proxy's
     * class as the class argument and those permissions. If <code>grant</code>
     * discovers that dynamic permission grants are not supported and throws a
     * {@link UnsupportedOperationException}, catches that exception and throws
     * a {@link SecurityException}. <p>
     *
     * Subclasses may wish to override this method, for example, to alter the
     * principals for which permission grants are made.
     *
     * @param proxy the proxy to grant permissions
     * @throws SecurityException if a security exception occurs
     * @throws NullPointerException if proxy is <code>null</code>
     * @see #prepareProxy prepareProxy
     * @see #getPermissions getPermissions
     * @see Security#grant(Class,Permission[]) Security.grant
     */
    protected void grant(Object proxy) {
	if (proxy == null) {
	    throw new NullPointerException("Proxy cannot be null");
	}
	Permission[] perms = getPermissions(proxy);
	if (perms.length > 0) {
	    try {
		Security.grant(proxy.getClass(), perms);
	    } catch (UnsupportedOperationException e) {
		SecurityException se = new SecurityException(
		    "Dynamic permission grants are not supported");
		se.initCause(e);
		throw se;
	    }
	}
    }

    /**
     * Sets constraints on the proxy. Called by the default implementation of
     * {@link #prepareProxy prepareProxy} unless {@link #verify(Object) verify}
     * or {@link #grant grant} throw an exception. <p>
     *
     * The default implementation provides the following behavior. If
     * <code>proxy</code> is <code>null</code>, throws a {@link
     * NullPointerException}. Otherwise, if {@link #methodConstraintsSpecified}
     * is <code>false</code>, returns the proxy, else if object does not
     * implement {@link RemoteMethodControl}, throws a {@link
     * SecurityException}, else returns the result of calling {@link
     * RemoteMethodControl#setConstraints RemoteMethodControl.setConstraints}
     * on the proxy, using the value returned from calling {@link
     * #getMethodConstraints getMethodConstraints} with <code>proxy</code>. <p>
     *
     * Subclasses may wish to override this method, for example, to support
     * verifying objects that do not implement {@link RemoteMethodControl}.
     *
     * @param proxy the proxy
     * @return the proxy with updated constraints
     * @throws NullPointerException if <code>proxy</code> is <code>null</code>
     * @throws SecurityException if a security exception occurs
     * @see #prepareProxy prepareProxy
     * @see #getMethodConstraints getMethodConstraints
     * @see RemoteMethodControl#setConstraints
     *	    RemoteMethodControl.setConstraints
     */
    protected Object setConstraints(Object proxy) {
	if (proxy == null) {
	    throw new NullPointerException("Proxy cannot be null");
	} else if (!methodConstraintsSpecified) {
	    return proxy;
	} else if (!(proxy instanceof RemoteMethodControl)) {
	    throw new SecurityException(
		"Proxy must implement RemoteMethodControl");
	} else {
	    return ((RemoteMethodControl) proxy).setConstraints(
		getMethodConstraints(proxy));
	}
    }

    /** Returns a string representation of this object. */
    public String toString() {
	String className = getClass().getName();
	int dot = className.lastIndexOf('.');
	if (dot >= 0) {
	    className = className.substring(dot + 1);
	}
	StringBuffer sb = new StringBuffer(className).append('[');
	if (verify) {
	    sb.append("verify");
	}
	if (methodConstraintsSpecified) {
	    if (verify) {
		sb.append(", ");
	    }
	    sb.append(methodConstraints);
	}
	if (permissions.length > 0) {
	    if (verify || methodConstraintsSpecified) {
		sb.append(", ");
	    }
	    sb.append('{');
	    for (int i = 0; i < permissions.length; i++) {
		if (i > 0) {
		    sb.append(", ");
		}
		sb.append(permissions[i]);
	    }
	    sb.append('}');
	}
	sb.append(']');
	return sb.toString();
    }

    /**
     * Returns <code>true</code> if the given object is an instance of the same
     * class as this object, with the same value for <code>verify</code>, with
     * method constraints that are <code>equals</code> or similarly not
     * specified, and with <code>permissions</code> containing the same
     * elements, independent of order.
     */
    public boolean equals(Object object) {
	if (this == object) {
	    return true;
	} else if (object == null || object.getClass() != getClass()) {
	    return false;
	}
	BasicProxyPreparer other = (BasicProxyPreparer) object;
	if (verify != other.verify) {
	    return false;
	}
	if (methodConstraintsSpecified != other.methodConstraintsSpecified) {
	    return false;
	} else if (methodConstraintsSpecified
		   && (methodConstraints == null
		       ? other.methodConstraints != null
		       : !methodConstraints.equals(other.methodConstraints)))
	{
	    return false;
	}
	if (permissions.length != other.permissions.length) {
	    return false;
	}
	/*
	 * Determine if the permissions contain the same elements, including
	 * duplicates.
	 */
	Permission[] otherPermissions =
	    (Permission[]) other.permissions.clone();
      top:
	for (int i = permissions.length; --i >= 0; ) {
	    Permission p = permissions[i];
	    for (int j = i; j >= 0; j--) {
		if (p.equals(otherPermissions[j])) {
		    otherPermissions[j] = otherPermissions[i];
		    continue top;
		}
	    }
	    return false;
	}
	return true;
    }

    /** Returns a hash code value for this object. */
    public int hashCode() {
	int hash = getClass().getName().hashCode();
	if (verify) {
	    hash += 1;
	}
	if (methodConstraintsSpecified) {
	    hash += 1<<16;
	    if (methodConstraints != null) {
		hash += methodConstraints.hashCode();
	    }
	}
	for (int i = permissions.length; --i >= 0; ) {
	    hash += permissions[i].hashCode();
	}
	return hash;
    }

    /**
     * Verifies that fields have legal values.
     *
     * @throws InvalidObjectException if
     *	       <code>methodConstraintsSpecified</code> is <code>false</code>
     *	       and <code>methodConstraints</code> is not <code>null</code>, if
     *	       <code>permissions</code> is <code>null</code>, or if
     *	       <code>permissions</code> is not <code>null</code> and any of its
     *	       elements are <code>null</code>
     * @throws IOException if an I/O error occurs
     * @throws ClassNotFoundException if the class of a serialized object could
     *	       not be found
     */
    private void readObject(ObjectInputStream s)
	throws IOException, ClassNotFoundException
    {
	s.defaultReadObject();
	if (!methodConstraintsSpecified && methodConstraints != null) {
	    throw new InvalidObjectException(
		"Method constraints not specified but not null");
	} 
	if (permissions == null) {
	    throw new InvalidObjectException("Permissions cannot be null");
	}
	for (int i = permissions.length; --i >= 0; ) {
	    if (permissions[i] == null) {
		throw new InvalidObjectException("Permission cannot be null");
	    }
	}
    }

    /**
     * Throws an exception to insure that data was supplied in order to set the
     * permissions field to an empty array.
     *
     * @throws InvalidObjectException whenever this method is called
     */
    private void readObjectNoData() throws InvalidObjectException {
	throw new InvalidObjectException("Permissions must be specified");
    }
}
