/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2004-2007 Bull S.A.S.
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.lib.security.jacc;

import java.net.SocketPermission;
import java.security.CodeSource;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Policy;
import java.security.Principal;
import java.security.ProtectionDomain;

import javax.security.jacc.EJBMethodPermission;
import javax.security.jacc.EJBRoleRefPermission;
import javax.security.jacc.PolicyConfiguration;
import javax.security.jacc.PolicyConfigurationFactory;
import javax.security.jacc.PolicyContext;
import javax.security.jacc.PolicyContextException;
import javax.security.jacc.WebResourcePermission;
import javax.security.jacc.WebRoleRefPermission;
import javax.security.jacc.WebUserDataPermission;

import org.ow2.jonas.lib.util.I18n;
import org.ow2.jonas.lib.util.Log;


import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;

/**
 * Defines the "delegating Policy provider" / JACC 2.5
 * In J2SE 1.4 new methods can be used for dynamic permissions
 * implies() and getPermissions() methods on Policy class were added.
 *
 * A replacement Policy object may accomplish this by delegating
 * non-javax.security.jacc policy decisions to the corresponding
 * default system Policy implementation class. A replacement
 * Policy object that relies in this way on the corresponding
 * default Policy implementation class must identify itself in
 * its installation instructions as a "delegating Policy provider"
 *
 * JOnAS uses delegating model
 * @author Florent Benoit
 */
public final class JPolicy extends Policy {

    /**
     * Logger
     */
    private static Logger logger = Log.getLogger(Log.JONAS_SECURITY_PREFIX);

    /**
     * Unique instance of JPolicy
     */
    private static JPolicy unique = null;

    /**
     * Bootstrap Policy provider use for delegating non-jacc decisions
     */
    private static Policy initialPolicy = null;

    /**
     * I18n
     */
    private static I18n i18n = I18n.getInstance(JPolicy.class);

    /**
     * Reference to the JOnAS PolicyConfigurationFactory
     * Used for retrieve parameters with interfaces not
     * in javax.security.jacc.PolicyConfigurationFactory
     */
    private static PolicyConfigurationFactory policyConfigurationFactory = null;


    /**
     * Constructor : build a policy which manage JACC permissions
     * The non-jacc permissions are delegated to the initial Policy class
     */
    public JPolicy() {
        // Retrieve existing policy
        initialPolicy = Policy.getPolicy();
    }

    /**
     * Init the PolicyConfiguration factory object used in Policy configuration
     * @throws JPolicyException if some methods on PolicyConfigurationFactory fail
     */
    private void initPolicyConfigurationFactory() throws JPolicyException {
        // Check that factory is the JOnAS policy configuration factory
        try {
            policyConfigurationFactory = PolicyConfigurationFactory.getPolicyConfigurationFactory();
        } catch (ClassNotFoundException cnfe) {
            throw new JPolicyException("PolicyConfigurationFactory class implementation was not found : '" + cnfe.getMessage() + "'.");
        } catch (PolicyContextException pce) {
            throw new JPolicyException("PolicyContextException in PolicyConfigurationFactory : '" + pce.getMessage() + "'.");
        }

    }


    /**
     * Gets the unique instance of the JACC delegating policy provider
     * @return unique instance of the JACC delegating policy provider
     */
    public static JPolicy getInstance() {
        if (unique == null) {
            unique = new JPolicy();
        }
        return unique;
    }



    // Section 4.8
    // J2EE 1.4 container can call Policy.implies or Policy.getPermissions
    // with an argument ProtectionDomain that was constructed with the
    // principals of the caller.
    // Then the caller must call implies method on the returned PermissionCollection


    /**
     * Evaluates the global policy for the permissions granted
     * to the ProtectionDomain and tests whether the permission is granted.
     * @param domain the ProtectionDomain to test.
     * @param permission the Permission object to be tested for implication.
     * @return true if "permission" is a proper subset of a permission
     *         granted to this ProtectionDomain.
     */
    public boolean implies(ProtectionDomain domain, Permission permission) {
        // See 2.5 of JACC. A replacement Policy object may accomplish this
        // by delegating non-javax.security.jacc policy decisions to the
        // corresponding default system Policy implementation class.

        if (permission instanceof RuntimePermission || permission instanceof SocketPermission) {
            return initialPolicy.implies(domain, permission);
        }


        // check with context ID
        String contextID = PolicyContext.getContextID();
        // No context, use existing
        if (contextID == null) {
            return initialPolicy.implies(domain, permission);
        }

        if (!(permission instanceof EJBMethodPermission || permission instanceof EJBRoleRefPermission
              || permission instanceof WebUserDataPermission || permission instanceof WebRoleRefPermission
              || permission instanceof WebResourcePermission)) {
            return initialPolicy.implies(domain, permission);
        }




        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "!= null, Permission being checked = " + permission);
        }

        // configuration was committed ?
        try {
            if (policyConfigurationFactory == null) {
                initPolicyConfigurationFactory();
            }

            if (!policyConfigurationFactory.inService(contextID)) {
                if (logger.isLoggable(BasicLevel.DEBUG)) {
                    logger.log(BasicLevel.DEBUG, "Not in service, return false");
                }
                return false;
            }
        } catch (JPolicyException jpe) {
            if (logger.isLoggable(BasicLevel.ERROR)) {
                logger.log(BasicLevel.ERROR, i18n.getMessage("JPolicy.implies.canNotCheck", jpe.getMessage()));
            }
            return false;
        } catch (PolicyContextException pce) {
            if (logger.isLoggable(BasicLevel.ERROR)) {
                logger.log(BasicLevel.ERROR, i18n.getMessage("JPolicy.implies.canNotCheck", pce.getMessage()));
            }
            return false;
        }

        JPolicyConfiguration jPolicyConfiguration = null;
        try {
            PolicyConfiguration pc = policyConfigurationFactory.getPolicyConfiguration(contextID, false);
            if (pc instanceof JPolicyConfiguration) {
                jPolicyConfiguration = (JPolicyConfiguration) pc;
            } else {
                //Maybe it's a delegating policy configuration and we have a configuration for this object
                jPolicyConfiguration = JPolicyConfigurationKeeper.getConfiguration(contextID);
                if (jPolicyConfiguration == null) {
                    throw new RuntimeException("This policy provider can only manage JPolicyConfiguration objects");
                }
            }
        } catch (PolicyContextException pce) {
            if (logger.isLoggable(BasicLevel.ERROR)) {
                logger.log(BasicLevel.ERROR, i18n.getMessage("JPolicy.implies.canNotRetrieve", contextID, pce.getMessage()));
            }
            return false;
        }

        /* JACC 3.2
           The provider must ensure that excluded policy statements take precedence
           over overlapping unchecked policy statements, and that both excluded and
           unchecked policy statements take precedence over overlapping role based policy
           statements.
        */
        PermissionCollection excludedPermissions = jPolicyConfiguration.getExcludedPermissions();
        PermissionCollection uncheckedPermissions = jPolicyConfiguration.getUncheckedPermissions();

        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "Check permission");
            logger.log(BasicLevel.DEBUG, "Excluded permissions = " + excludedPermissions);
            logger.log(BasicLevel.DEBUG, "unchecked permissions = " + uncheckedPermissions);
        }

        // excluded ?
        if (excludedPermissions.implies(permission)) {
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "Permission '" + permission + "' is excluded, return false");
            }
            return false;
        } else if (uncheckedPermissions.implies(permission)) { // unchecked
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "Permission '" + permission + "' is unchecked, return true");
            }
            return true;
        } else {
            // per role if any or false
            if (domain.getPrincipals().length > 0) {
                if (logger.isLoggable(BasicLevel.DEBUG)) {
                    logger.log(BasicLevel.DEBUG, "There are principals, checking principals...");
                }
                // check roles
                return isImpliedPermissionForPrincipals(jPolicyConfiguration, permission, domain.getPrincipals());
            } else {
                if (logger.isLoggable(BasicLevel.DEBUG)) {
                    logger.log(BasicLevel.DEBUG, "Principals length = 0, there is no principal on this domain");
                }
                // permission not found
                if (logger.isLoggable(BasicLevel.DEBUG)) {
                    logger.log(BasicLevel.DEBUG, "Permission '" + permission + "' not found, return false");
                }
                return false;
            }
        }
    }


    /**
     * Evaluates the global policy and returns a PermissionCollection
     * object specifying the set of permissions allowed given the
     * characteristics of the protection domain.
     * @param domain the ProtectionDomain associated with the caller.
     * @return the set of permissions allowed for the domain according
     *         to the policy.The returned set of permissions must be a
     *         new mutable instance and it must support heterogeneous
     *         Permission types.
     */
    public PermissionCollection getPermissions(ProtectionDomain domain) {

         // Always use delegating model
        return initialPolicy.getPermissions(domain);

        //TODO : retrieve all permissions for a given ProtectionDomain
        //JPolicyConfiguration jPolicyConfiguration = policyConfigurationFactory.getPolicyConfiguration(contextID);
        //return jPolicyConfiguration.getPermissionsForDomain(domain);
        //         String contextID = PolicyContext.getContextID();
        //         // Delegating model
        //         if (contextID == null) {
        //             return initialPolicy.getPermissions(domain);
        //         } else {
        //             // not implemented as not recommended in section 4.8
        //             throw new UnsupportedOperationException("Method getPermissions with a given protection domain is not supported");
        //         }

    }


    /**
     * Evaluates the global policy and returns a PermissionCollection
     * object specifying the set of permissions allowed for code from
     * the specified code source.
     * @param codeSource the CodeSource 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
     *         codesource according to the policy.The returned
     *         set of permissions must be a new mutable instance
     *         and it must support heterogeneous Permission types.
     */
    public PermissionCollection getPermissions(CodeSource codeSource) {

        // Always use delegating model
        return initialPolicy.getPermissions(codeSource);

        //TODO : retrieve all permissions for a given codesource
        //         String contextID = PolicyContext.getContextID();
        //         if (contextID == null) {
        //             return initialPolicy.getPermissions(codeSource);
        //         } else {
        //             // not implemented
        //             throw new UnsupportedOperationException("Method getPermissions with a given codeSource is not implemented");
        //         }
    }


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



    /**
     * Check for each principal permission if the given permission is implied
     * @param jPolicyConfiguration JOnAS JACC PolicyConfiguration object
     * @param permission the permission to check
     * @param principals the array of principals on which we must retrieve permissions
     * @return true if the given permission is implied by a role's permission
     */
    private boolean isImpliedPermissionForPrincipals(JPolicyConfiguration jPolicyConfiguration, Permission permission, Principal[] principals) {
        //if (logger.isLoggable(BasicLevel.DEBUG)) {
        //    logger.log(BasicLevel.DEBUG, "");
        //}
        PermissionCollection permissions = null;
        int i = 0;
        boolean implied = false;
        // for each principal's permissions check if the given permission is implied
        while (i < principals.length && !implied) {
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "Checking permission '" + permission + "' with permissions of Principal '" + principals[i].getName() + "'.");
            }
            permissions = jPolicyConfiguration.getPermissionsForPrincipal(principals[i]);

            if (permissions.implies(permission)) {
                if (logger.isLoggable(BasicLevel.DEBUG)) {
                    logger.log(BasicLevel.DEBUG, "Permission implied with principal '" + principals[i].getName() + "'.");
                }
                implied = true;
            }
            i++;
        }
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            if (!implied) {
                logger.log(BasicLevel.DEBUG, "Permission '" + permission + "' was not found in each permissions of the given roles, return false");
            }
        }
        return implied;
    }

}

