/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2004 Bull S.A.
 * Contact: jonas-team@objectweb.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
 *
 * Initial developer: Florent BENOIT
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.objectweb.jonas_ejb.container;

import java.io.File;
import java.net.URI;
import java.net.URL;
import java.security.CodeSource;
import java.security.PermissionCollection;
import java.security.Principal;
import java.security.ProtectionDomain;
import java.security.cert.Certificate;
import java.util.Iterator;

import javax.security.jacc.EJBMethodPermission;
import javax.security.jacc.EJBRoleRefPermission;
import javax.security.jacc.PolicyContext;
import javax.security.jacc.PolicyContextException;

import org.objectweb.jonas.security.jacc.JPolicyContextHandlerCurrent;
import org.objectweb.jonas.security.jacc.JPolicyContextHandlerData;
import org.objectweb.jonas_ejb.deployment.api.BeanDesc;
import org.objectweb.jonas_ejb.deployment.api.DeploymentDesc;
import org.objectweb.jonas_ejb.deployment.api.ExcludeListDesc;
import org.objectweb.jonas_ejb.deployment.api.MethodPermissionDesc;
import org.objectweb.jonas_ejb.lib.EJBInvocation;
import org.objectweb.jonas_lib.deployment.api.SecurityRoleRefDesc;
import org.objectweb.jonas_lib.security.AbsPermissionManager;
import org.objectweb.jonas_lib.security.PermissionManagerException;
import org.objectweb.security.context.SecurityContext;
import org.objectweb.security.context.SecurityCurrent;
import org.objectweb.util.monolog.api.BasicLevel;

/**
 * Defines a PermissionManager class which will manage JACC permissions for an
 * ejbjar
 * @author Florent Benoit
 */
public class PermissionManager extends AbsPermissionManager {

    /**
     * Deployment desc of the module
     */
    private DeploymentDesc ejbDeploymentDesc = null;

    /**
     * Default Constructor
     * @param ejbDeploymentDesc EJB deployment Descriptor
     * @param contextId context ID used for PolicyContext
     * @throws PermissionManagerException if permissions can't be set
     */
    public PermissionManager(DeploymentDesc ejbDeploymentDesc, String contextId) throws PermissionManagerException {
        super(contextId);
        this.ejbDeploymentDesc = ejbDeploymentDesc;
    }

    /**
     * 3.1.5 Translating EJB Deployment Descriptors A reference to a
     * PolicyConfiguration object must be obtained by calling the
     * getPolicyConfiguration method on the PolicyConfigurationFactory
     * implementation class of the provider configured into the container. The
     * policy context identifier used in the call to getPolicyConfiguration must
     * be a String that satisfies the requirements described in Section 3.1.4,
     * EJB Policy Context Identifiers, on page 28. The value true must be passed
     * as the second parameter in the call to getPolicyConfiguration to ensure
     * that any and all policy statements are removed from the policy context
     * associated with the returned PolicyConfiguration. The method-permission,
     * exclude-list, and security-role-ref elements appearing in the deployment
     * descriptor must be translated into permissions and added to the
     * PolicyConfiguration object to yield an equivalent translation as that
     * defined in the following sections and such that every EJB method for
     * which the container performs pre-dispatch access decisions is implied by
     * at least one permission resulting from the translation.
     * @throws PermissionManagerException if permissions can't be set
     */
    public void translateEjbDeploymentDescriptor() throws PermissionManagerException {
        translateEjbMethodPermission();
        translateEjbExcludeList();
        translateEjbSecurityRoleRef();
    }

    /**
     * 3.1.5.1 Translating EJB method-permission Elements For each method
     * element of each method-permission element, an EJBMethodPermission object
     * translated from the method element must be added to the policy statements
     * of the PolicyConfiguration object. The name of each such
     * EJBMethodPermission object must be the ejb-name from the corresponding
     * method element, and the actions must be established by translating the
     * method element into a method specification according to the methodSpec
     * syntax defined in the documentation of the EJBMethodPermission class. The
     * actions translation must preserve the degree of specificity with respect
     * to method-name, method-intf, and method-params inherent in the method
     * element. If the method-permission element contains the unchecked element,
     * then the deployment tools must call the addToUncheckedPolicy method to
     * add the permissions resulting from the translation to the
     * PolicyConfiguration object. Alternatively, if the method-permission
     * element contains one or more role-name elements, then the deployment
     * tools must call the addToRole method to add the permissions resulting
     * from the translation to the corresponding roles of the
     * PolicyConfiguration object.
     * @throws PermissionManagerException if permissions can't be set
     */
    protected void translateEjbMethodPermission() throws PermissionManagerException {
        if (ejbDeploymentDesc == null || getPolicyConfiguration() == null) {
            throw new PermissionManagerException("PolicyConfiguration or ejbDeploymentDesc is null");
        }

        MethodPermissionDesc methodPermissionDesc = null;
        PermissionCollection permissionCollection = null;

        for (Iterator it = ejbDeploymentDesc.getMethodPermissionsDescList().iterator(); it.hasNext();) {
            methodPermissionDesc = (MethodPermissionDesc) it.next();
            permissionCollection = methodPermissionDesc.getEJBMethodPermissions();
            try {
                // unchecked or roles
                if (methodPermissionDesc.isUnchecked()) {
                    getPolicyConfiguration().addToUncheckedPolicy(permissionCollection);
                } else {
                    for (Iterator rolesIt = methodPermissionDesc.getRoleNameList().iterator(); rolesIt.hasNext();) {
                        getPolicyConfiguration().addToRole((String) rolesIt.next(), permissionCollection);
                    }
                }
            } catch (PolicyContextException pce) {
                throw new PermissionManagerException("Can not add add excluded policy", pce);
            }
        }
    }

    /**
     * 3.1.5.2 Translating the EJB exclude-list An EJBMethodPermission object
     * must be created for each method element occurring in the exclude-list
     * element of the deployment descriptor. The name and actions of each
     * EJBMethodPermission must be established as described in Section 3.1.5.1,
     * Translating EJB method-permission Elements. The deployment tools must use
     * the addToExcludedPolicy method to add the EJBMethodPermission objects
     * resulting from the translation of the exclude-list to the excluded policy
     * statements of the PolicyConfiguration object.
     * @throws PermissionManagerException if permissions can't be set
     */
    protected void translateEjbExcludeList() throws PermissionManagerException {
        if (ejbDeploymentDesc == null || getPolicyConfiguration() == null) {
            throw new PermissionManagerException("PolicyConfiguration or ejbDeploymentDesc is null");
        }
        ExcludeListDesc excludeListDesc = ejbDeploymentDesc.getExcludeListDesc();
        if (excludeListDesc != null) {
            try {
                getPolicyConfiguration().addToExcludedPolicy(excludeListDesc.getEJBMethodPermissions());
            } catch (PolicyContextException pce) {
                throw new PermissionManagerException("Can not add add excluded policy", pce);
            }
        }
    }

    /**
     * 3.1.5.3 Translating EJB security-role-ref Elements For each
     * security-role-ref element appearing in the deployment descriptor, a
     * corresponding EJBRoleRefPermission must be created. The name of each
     * EJBRoleRefPermission must be obtained as described for
     * EJBMethodPermission objects. The actions used to construct the permission
     * must be the value of the role-name (that is the reference), appearing in
     * the security-role-ref. The deployment tools must call the addToRole
     * method on the PolicyConfiguration object to add a policy statement
     * corresponding to the EJBRoleRefPermission to the role identified in the
     * rolelink appearing in the security-role-ref.
     * @throws PermissionManagerException if permissions can't be set
     */
    public void translateEjbSecurityRoleRef() throws PermissionManagerException {
        if (ejbDeploymentDesc == null || getPolicyConfiguration() == null) {
            throw new PermissionManagerException("PolicyConfiguration or ejbDeploymentDesc is null");
        }

        SecurityRoleRefDesc securityRoleRefDesc = null;
        BeanDesc beanDesc = null;

        // Add EJBRoleRefPermission for each bean
        for (Iterator itEjb = ejbDeploymentDesc.getBeanDescIterator(); itEjb.hasNext();) {
            beanDesc = (BeanDesc) itEjb.next();
            for (Iterator it = beanDesc.getSecurityRoleRefDescList().iterator(); it.hasNext();) {
                securityRoleRefDesc = (SecurityRoleRefDesc) it.next();
                try {
                    getPolicyConfiguration().addToRole(securityRoleRefDesc.getRoleLink(),
                            securityRoleRefDesc.getEJBRoleRefPermission());
                } catch (PolicyContextException pce) {
                    throw new PermissionManagerException("Can not add add excluded policy", pce);
                }
            }
        }
    }

    /**
     * Check the security for a given EJB signature method and for an EJB
     * @param ejbName name of the EJB
     * @param ejbInv object containing security signature of the method, args of
     *        method, etc
     * @param inRunAs bean calling this method is running in run-as mode or not ?
     * @return true if access to specific method is granted, else false.
     */
    public boolean checkSecurity(String ejbName, EJBInvocation ejbInv, boolean inRunAs) {
        try {
            PolicyContext.setContextID(getContextId());
            String methodSignature = ejbInv.methodPermissionSignature;

            if (TraceEjb.isDebugSecurity()) {
                TraceEjb.security.log(BasicLevel.DEBUG, "EjbName = " + ejbName + ", methodSignature = " + methodSignature);
            }

            // Set the information for the Policy provider
            JPolicyContextHandlerData jPolicyContextHandlerData = JPolicyContextHandlerCurrent.getCurrent()
                    .getJPolicyContextHandlerData();
            if (jPolicyContextHandlerData == null) {
                TraceEjb.security.log(BasicLevel.ERROR, "The Handler data retrieved is null !");
                return false;
            }
            jPolicyContextHandlerData.setEjbArguments(ejbInv.arguments);
            jPolicyContextHandlerData.setProcessingBean(ejbInv.bean);

            PolicyContext.setHandlerData(jPolicyContextHandlerData);

            // Build Protection Domain with a codesource and array of principal
            URI uri = new File(getContextId()).toURI();
            CodeSource codesource = new CodeSource(new URL(uri.toString()), (Certificate[]) null);
            SecurityCurrent current = SecurityCurrent.getCurrent();
            final SecurityContext sctx = current.getSecurityContext();
            if (TraceEjb.isDebugSecurity()) {
                TraceEjb.security.log(BasicLevel.DEBUG, "Security Context = " + sctx);
                if (sctx != null) {
                    TraceEjb.security.log(BasicLevel.DEBUG, "sctx.getCallerPrincipalRoles() = " + sctx.getCallerPrincipalRoles(inRunAs));
                }
            }


            String runAsRole = null;

            Principal[] principals = null;
            String[] runAsPrincipalRoles = null;
            String[] principalRoles = null;
            if (sctx != null) {
                synchronized (sctx) {
                    runAsRole = sctx.peekRunAsRole();
                    runAsPrincipalRoles = sctx.peekRunAsPrincipalRoles();
                    principalRoles = sctx.getCallerPrincipalRoles(inRunAs);
                }
                if (runAsRole != null) {
                    principals = new Principal[runAsPrincipalRoles.length];
                    for (int k = 0; k < runAsPrincipalRoles.length; k++) {
                        principals[k] = new org.objectweb.jonas.security.auth.JPrincipal(
                                runAsPrincipalRoles[k]);
                    }
                } else {
                    principals = new Principal[principalRoles.length];
                    for (int k = 0; k < principalRoles.length; k++) {
                        principals[k] = new org.objectweb.jonas.security.auth.JPrincipal(
                                principalRoles[k]);
                    }
               }
            } else {
                if (TraceEjb.isDebugSecurity()) {
                    TraceEjb.security.log(BasicLevel.DEBUG, "Security context is null");
                }
            }
            ProtectionDomain protectionDomain = new ProtectionDomain(codesource, null, null, principals);

            //TODO : cache ejbName/methodSignature to avoid creation of a new
            // EJBMethodPermission each time
            // See JACC 4.12
            EJBMethodPermission ejbMethodPermission = new EJBMethodPermission(ejbName, methodSignature);
            boolean accessOK = getPolicy().implies(protectionDomain, ejbMethodPermission);
            if (TraceEjb.isDebugSecurity()) {
                TraceEjb.security.log(BasicLevel.DEBUG, "Policy.implies result = " + accessOK);
            }
            jPolicyContextHandlerData = null;
            return accessOK;

        } catch (Exception e) {
            TraceEjb.security.log(BasicLevel.ERROR, "Cannot check security", e);
            return false;
        }

    }

    /**
     * Test if the caller has a given role. EJBRoleRefPermission object must be
     * created with ejbName and actions equal to roleName
     * @see section 4.3.2 of JACC
     * @param ejbName The name of the EJB on wich look role
     * @param roleName The name of the security role. The role must be one of
     *        the security-role-ref that is defined in the deployment
     *        descriptor.
     * @param inRunAs bean calling this method is running in run-as mode or not ?
     * @return True if the caller has the specified role.
     */
    public boolean isCallerInRole(String ejbName, String roleName, boolean inRunAs) {
        try {
            PolicyContext.setContextID(getContextId());
            if (TraceEjb.isDebugSecurity()) {
                TraceEjb.security.log(BasicLevel.DEBUG, "roleName = " + roleName);
            }

            // Build Protection Domain with a codesource and array of principal
            URI uri = new URI("file://" + getContextId());
            CodeSource codesource = new CodeSource(new URL(uri.toString()), (Certificate[]) null);
            SecurityCurrent current = SecurityCurrent.getCurrent();
            final SecurityContext sctx = current.getSecurityContext();
            if (TraceEjb.isDebugSecurity()) {
                TraceEjb.security.log(BasicLevel.DEBUG, "Security Context = " + sctx);
                TraceEjb.security.log(BasicLevel.DEBUG, "sctx.getCallerPrincipalRoles() = "
                        + sctx.getCallerPrincipalRoles(inRunAs));
            }
            Principal[] principals = null;
            if (sctx != null) {
                principals = new Principal[sctx.getCallerPrincipalRoles(inRunAs).length];
                for (int k = 0; k < sctx.getCallerPrincipalRoles(inRunAs).length; k++) {
                    principals[k] = new org.objectweb.jonas.security.auth.JPrincipal(sctx.getCallerPrincipalRoles(inRunAs)[k]);
                }
            } else {
                if (TraceEjb.isDebugSecurity()) {
                    TraceEjb.security.log(BasicLevel.DEBUG, "Security context is null");
                }
            }
            ProtectionDomain protectionDomain = new ProtectionDomain(codesource, null, null, principals);

            //TODO :add cache mechanism
            // See JACC 4.12
            EJBRoleRefPermission ejbRoleRefPermission = new EJBRoleRefPermission(ejbName, roleName);
            boolean isInRole = getPolicy().implies(protectionDomain, ejbRoleRefPermission);
            if (TraceEjb.isDebugSecurity()) {
                TraceEjb.security.log(BasicLevel.DEBUG, "Policy.implies result = " + isInRole);
            }
            return isInRole;

        } catch (Exception e) {
            TraceEjb.security.log(BasicLevel.ERROR, "Cannot check isCallerInRole", e);
            return false;
        }

    }

    /**
     * Reset Deployment Descriptor
     */
    protected void resetDeploymentDesc() {
        ejbDeploymentDesc = null;
    }

}