/**
 * 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.web.base.lib;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.CodeSource;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Principal;
import java.security.ProtectionDomain;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
 
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 javax.servlet.http.HttpServletRequest;

import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;
import org.ow2.jonas.deployment.common.SecurityRoleRefDesc;
import org.ow2.jonas.deployment.web.SecurityConstraintListDesc;
import org.ow2.jonas.deployment.web.SecurityRoleDesc;
import org.ow2.jonas.deployment.web.ServletDesc;
import org.ow2.jonas.deployment.web.WebContainerDeploymentDesc;
import org.ow2.jonas.lib.security.AbsPermissionManager;
import org.ow2.jonas.lib.security.PermissionManagerException;
import org.ow2.jonas.lib.security.auth.JPrincipal;
import org.ow2.jonas.lib.security.jacc.handlers.JPolicyContextHandlerCurrent;
import org.ow2.jonas.lib.security.jacc.handlers.JPolicyContextHandlerData;
import org.ow2.jonas.lib.security.mapping.JPolicyUserRoleMapping;
import org.ow2.jonas.lib.util.Log;

/**
 * Defines a PermissionManager class which will manage JACC permissions for a
 * web-app.
 * @author Florent Benoit
 * @author S. Ali Tokmen
 */
public class PermissionManager extends AbsPermissionManager {

    /**
     * Logger used.
     */
    private static Logger logger = null;

    /**
     * Deployment desc of the module.
     */
    private WebContainerDeploymentDesc webContainerDeploymentDesc = null;

    /**
     * Default Constructor.
     * @param webContainerDeploymentDesc EJB deployment Descriptor
     * @param contextId context ID used for PolicyContext
     * @param remove - remove the policy context when creating a new one.
     * @throws PermissionManagerException if permissions can't be set
     */
    public PermissionManager(final WebContainerDeploymentDesc webContainerDeploymentDesc,
                             final String contextId,
                             final boolean remove)
            throws PermissionManagerException {
        super(contextId, remove);
        this.webContainerDeploymentDesc = webContainerDeploymentDesc;
        logger = Log.getLogger(Log.JONAS_WEB_PREFIX);
    }

    /**
     * 3.1.3.Translating Servlet 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 the getPolicyConfiguration
     * method must be a String composed as described in Section 3.1.2, Servlet
     * Policy Context Identifiers, on page 19. 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 security-constraint
     * and securityrole-ref elements in the deployment descriptor must be
     * translated into permissions and added to the PolicyConfiguration object
     * as defined in the following sections.
     * @throws PermissionManagerException if permissions can't be set
     */
    public void translateServletDeploymentDescriptor() throws PermissionManagerException {
        translateSecurityConstraintElements();
        translateServletSecurityRoleRef();
    }

    /**
     * 3.1.3.1 Translating security-constraint elements The paragraphs of this
     * section describe the translation of security-constraints into
     * WebResourcePermission and WebUserDataPermission objects constructed using
     * qualified URL pattern names. In the exceptional case, as defined in
     * Qualified URL Pattern Names, where a pattern is made irrelevant by a
     * qualifying pattern, the permission instantiations that would result from
     * the translation of the pattern, as described below, must not be
     * performed. Otherwise, the translation of URL patterns in security
     * constraints must yield an equivalent translation to the translation that
     * would result from following the instructions in the remainder of this
     * section. [...]
     * @throws PermissionManagerException if permissions can't be set
     */
    protected void translateSecurityConstraintElements() throws PermissionManagerException {
        if (webContainerDeploymentDesc == null || getPolicyConfiguration() == null) {
            throw new PermissionManagerException("PolicyConfiguration or webContainerbDeploymentDesc is null");
        }

        SecurityConstraintListDesc securityConstraintListDesc = webContainerDeploymentDesc
                .getSecurityConstraintListDesc();
        PermissionCollection excludedPermissions = securityConstraintListDesc.getExcludedPermissions();
        PermissionCollection uncheckedPermissions = securityConstraintListDesc.getUncheckedPermissions();
        PermissionCollection rolePermissions = null;
        String roleName = null;
        Map roleMapPermissions = securityConstraintListDesc.getPermissionsByRole();

        try {
            getPolicyConfiguration().addToExcludedPolicy(excludedPermissions);
            getPolicyConfiguration().addToUncheckedPolicy(uncheckedPermissions);

            for (Iterator rolesIt = roleMapPermissions.keySet().iterator(); rolesIt.hasNext();) {
                roleName = (String) rolesIt.next();
                rolePermissions = (PermissionCollection) roleMapPermissions.get(roleName);
                getPolicyConfiguration().addToRole(roleName, rolePermissions);
            }
        } catch (PolicyContextException pce) {
            throw new PermissionManagerException("Can not add add permissions to policy", pce);
        }
    }

    /**
     * 3.1.3.2 Translating Servlet security-role-ref Elements For each
     * security-role-ref appearing in the deployment descriptor a corresponding
     * WebRoleRefPermission must be added to the corresponding role. The name of
     * the WebRoleRefPermission must be the servlet-name in whose context the
     * security-role-ref is defined. The actions of the WebRoleRefPermission
     * 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 the WebRoleRefPermission
     * object resulting from the translation to the role identified in the
     * role-link appearing in the security-role-ref. Additional
     * WebRoleRefPermission objects must be added to the PolicyConfiguration as
     * follows. For each servlet element in the deployment descriptor a
     * WebRoleRefPermission must be added to each security-role whose name does
     * not appear as the role-name in a security-role-ref within the servlet
     * element. The name of each such WebRoleRefPermission must be the
     * servlet-name of the corresponding servlet element. The actions (that is,
     * reference) of each such WebRoleRefPermission must be the corresponding
     * (non-appearing) role-name. The resulting permissions must be added to the
     * corresponding roles by calling the addToRole method on the
     * PolicyConfiguration object.
     * @throws PermissionManagerException if permissions can't be set
     */
    protected void translateServletSecurityRoleRef() throws PermissionManagerException {
        if (webContainerDeploymentDesc == null || getPolicyConfiguration() == null) {
            throw new PermissionManagerException("PolicyConfiguration or webContainerbDeploymentDesc is null");
        }

        Collection servlets = webContainerDeploymentDesc.getServletDescList();

        // List of roles which appear as role-name in a security-role-ref
        List rolesAppearedInSecurityRoleRef = new ArrayList();

        /*
         * For each security-role-ref appearing in the deployment descriptor a
         * corresponding WebRoleRefPermission must be added to the corresponding
         * role. The name of the WebRoleRefPermission must be the servlet-name
         * in whose context the security-role-ref is defined. The actions of the
         * WebRoleRefPermission must be the value of the role-name (that is the
         * reference), appearing in the security-role-ref
         */
        ServletDesc servletDesc = null;
        String servletName = null;
        List roleRefs = null;
        SecurityRoleRefDesc securityRoleRefDesc = null;
        for (Iterator itServlet = servlets.iterator(); itServlet.hasNext();) {
            servletDesc = (ServletDesc) itServlet.next();
            roleRefs = servletDesc.getSecurityRoleRefList();
            servletName = servletDesc.getServletName();
            for (Iterator itRoleRef = roleRefs.iterator(); itRoleRef.hasNext();) {
                securityRoleRefDesc = (SecurityRoleRefDesc) itRoleRef.next();
                /*
                 * The deployment tools must call the addToRole method on the
                 * PolicyConfiguration object to add the WebRoleRefPermission
                 * object resulting from the translation to the role identified
                 * in the role-link appearing in the security-role-ref.
                 */
                Permission webRoleRefPermission = securityRoleRefDesc.getWebRoleRefPermission();

                // Role has appear
                rolesAppearedInSecurityRoleRef.add(securityRoleRefDesc.getRoleName());

                try {
                    getPolicyConfiguration().addToRole(securityRoleRefDesc.getRoleLink(), webRoleRefPermission);
                } catch (PolicyContextException pce) {
                    throw new PermissionManagerException("Can not add add permission '" + webRoleRefPermission
                            + "' to policy", pce);
                }
            }
        }

        /*
         * Additional WebRoleRefPermission objects must be added to the
         * PolicyConfiguration as follows. For each servlet element in the
         * deployment descriptor a WebRoleRefPermission must be added to each
         * security-role whose name does not appear as the role-name in a
         * security-role-ref within the servlet element.
         */
        List securityRoles = webContainerDeploymentDesc.getSecurityRoleList();
        SecurityRoleDesc securityRoleDesc = null;
        String securityRoleName = null;

        for (Iterator itServlet = servlets.iterator(); itServlet.hasNext();) {
            servletDesc = (ServletDesc) itServlet.next();
            servletName = servletDesc.getServletName();

            for (Iterator itSecurityRoles = securityRoles.iterator(); itSecurityRoles.hasNext();) {
                securityRoleDesc = (SecurityRoleDesc) itSecurityRoles.next();
                securityRoleName = securityRoleDesc.getRoleName();
                // if role does not appear as the role-name in a
                // security-role-ref
                // need to create permission
                if (!rolesAppearedInSecurityRoleRef.contains(securityRoleName)) {

                    /*
                     * The name of each such WebRoleRefPermission must be the
                     * servlet-name of the corresponding servlet element. The
                     * actions (that is, reference) of each such
                     * WebRoleRefPermission must be the corresponding
                     * (non-appearing) role-name. The resulting permissions must
                     * be added to the corresponding roles by calling the
                     * addToRole method on the PolicyConfiguration object.
                     */
                    Permission webRoleRefPermission = new WebRoleRefPermission(servletName, securityRoleName);
                    try {
                        getPolicyConfiguration().addToRole(securityRoleName, webRoleRefPermission);
                    } catch (PolicyContextException pce) {
                        throw new PermissionManagerException("Can not add add permission '" + webRoleRefPermission
                                + "' to policy", pce);
                    }
                }
            }
        }

        /**
         * For resolution B19 added for maintenance review, WebRoleRefPermission
         * must be added with empty strings for every security role
         */
        securityRoles = webContainerDeploymentDesc.getSecurityRoleList();
        for (Iterator itSecurityRoles = securityRoles.iterator(); itSecurityRoles.hasNext();) {
            securityRoleDesc = (SecurityRoleDesc) itSecurityRoles.next();
            securityRoleName = securityRoleDesc.getRoleName();
            /**
             * Add permission with empty name
             * B19 resolution
             */
            Permission webRoleRefPermission = new WebRoleRefPermission("", securityRoleName);
            try {
                getPolicyConfiguration().addToRole(securityRoleName, webRoleRefPermission);
            } catch (PolicyContextException pce) {
                throw new PermissionManagerException("Can not add add permission '" + webRoleRefPermission
                        + "' to policy", pce);
            }
        }
    }

    /**
     * Check the security for a given HttpServletRequest.
     * @param request the http servlet request
     * @param principalName name of the principal
     * @param roles array of roles for this permission
     * @return true if the permission is granted, else false
     */
    public boolean checkWebUserDataPermission(final HttpServletRequest request,
                                              final String principalName,
                                              final String[] roles) {

        try {
            ProtectionDomain protectionDomain = initPolicyContext(request, principalName, roles);

            //TODO : cache request to avoid creation of a new
            // WebUserDatapermission each time
            // See JACC 4.12
            WebUserDataPermission webUserDataPermission = new WebUserDataPermission(request);
            boolean accessOK = getPolicy().implies(protectionDomain, webUserDataPermission);

            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "Policy.implies result = " + accessOK);
            }
            return accessOK;

        } catch (Exception e) {
            logger.log(BasicLevel.ERROR, "Can't check web user data permission", e);
            return false;
        }

    }

    /**
     * Check the security for a given HttpServletRequest.
     * @param request the http servlet request
     * @param principalName name of the principal
     * @param roles array of roles for this permission
     * @return true if the permission is granted, else false
     */
    public boolean checkWebResourcePermission(final HttpServletRequest request,
                                              final String principalName,
                                              final String[] roles) {
        try {
            ProtectionDomain protectionDomain = initPolicyContext(request, principalName, roles);

            //TODO : cache request to avoid creation of a new
            // WebResourcePermission each time
            // See JACC 4.12
            WebResourcePermission webResourcePermission = new WebResourcePermission(request);
            boolean accessOK = getPolicy().implies(protectionDomain, webResourcePermission);
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "Policy.implies result = " + accessOK);
            }
            return accessOK;

        } catch (Exception e) {
            logger.log(BasicLevel.ERROR, "Can't check web resource permission :" + e.getMessage());
            return false;
        }

    }

    /**
     * Check the security for a given HttpServletRequest.
     * @param request the http servlet request
     * @param servletName Name of the servlet
     * @param principalName name of the principal
     * @param roles array of roles for this permission
     * @param roleName name of the role
     * @return true if the permission is granted, else false
     */
    public boolean checkWebRoleRefPermission(final HttpServletRequest request,
                                             final String servletName,
                                             final String principalName,
                                             final String[] roles,
                                             final String roleName) {
        try {

            ProtectionDomain protectionDomain = initPolicyContext(request, principalName, roles);

            //TODO : cache request to avoid creation of a new
            // WebRoleRefPermission each time
            // See JACC 4.12
            WebRoleRefPermission webRoleRefPermission = new WebRoleRefPermission(servletName, roleName);
            boolean accessOK = getPolicy().implies(protectionDomain, webRoleRefPermission);
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "Policy.implies result = " + accessOK);
            }
            return accessOK;

        } catch (Exception e) {
            logger.log(BasicLevel.ERROR, "Can't check web role ref permission :" + e.getMessage());
            return false;
        }

    }

    /**
     * Init the PolicyContext and return a protectionDomain.
     * @param request the http servlet request
     * @param principalName name of the principal
     * @param roles array of roles for this permission
     * @return the protection domain built with given arguments
     * @throws URISyntaxException if the URI can't be built
     * @throws MalformedURLException if the URL of the codesource can't be built
     */
    private synchronized ProtectionDomain initPolicyContext(final HttpServletRequest request,
                                                            final String principalName,
                                                            final String[] roles)
            throws URISyntaxException, MalformedURLException {

        // Set the policy context
        PolicyContext.setContextID(getContextId());

        // Configure ContextHandler
        JPolicyContextHandlerData jPolicyContextHandlerData = JPolicyContextHandlerCurrent.getCurrent()
                .getJPolicyContextHandlerData();
        if (jPolicyContextHandlerData == null) {
            logger.log(BasicLevel.ERROR, "The Handler data retrieved is null !");
            return null;
        }
        jPolicyContextHandlerData.setHttpServletRequest(request);
        PolicyContext.setHandlerData(jPolicyContextHandlerData);

        // Build Protection Domain with a codesource and array of principal
        // We need to switch between URIs and URLs in order to escape spacing characters
        String urlString = "file://" + getContextId();
        URL url;
        try {
            // Check if the URL is valid, therefore already escaped correctly
            url = new URI(new URL(urlString).toString()).toURL();
        } catch(URISyntaxException e) {
            // Given URL wasn't escaped correctly, escape spaces
            url = new URI(new URL(urlString.replaceAll("\\ ", "%20")).toString()).toURL();
        }
        CodeSource codesource = new CodeSource(url, (Certificate[]) null);

        // Existing mapping ?
        String[] overridedRoles = JPolicyUserRoleMapping.getMappingForPrincipal(getContextId(), principalName);
        String[] allRoles = roles;
        if (overridedRoles != null) {
            allRoles = overridedRoles;
        }

        Principal[] principals = null;
        if (allRoles != null) {
            principals = new Principal[allRoles.length];
            for (int k = 0; k < allRoles.length; k++) {
                principals[k] = new JPrincipal(allRoles[k]);
            }
        }

        return new ProtectionDomain(codesource, null, null, principals);
    }

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

}
