/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2006-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: RoleBasedAuthorizationModule.java 21066 2011-03-27 18:12:16Z alitokmen $
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.security.auth.spi;

import java.io.FileInputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.security.AccessController;
import java.security.Principal;
import java.security.acl.Group;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import javax.management.MBeanServer;
import javax.management.remote.MBeanServerForwarder;
import javax.security.auth.Subject;

import org.ow2.jonas.jmx.JmxService;
import org.ow2.jonas.lib.bootstrap.JProp;

/**
 * Authorizes a certain user to access a certain resource based on the role(s)
 * of that user and that user's group(s).
 *
 * @author S. Ali Tokmen
 */
public class RoleBasedAuthorizationModule implements InvocationHandler {
    /**
     * Returns a usable MBeanServerForwarder instance.
     *
     * @param type   MBeanServerForwarder type.
     * @param param  MBeanServerForwarder parameter, for example the access
     *               list file for the type "file".
     *
     * @return A usable MBeanServerForwarder instance.
     */
    public static MBeanServerForwarder newProxyInstance(final String type, final String param) {
        try {
            if ("file".equals(type)) {
                // Make sure files get read using JONAS_BASE
                String filename = JProp.getJonasBase() + System.getProperty("file.separator") + param;
                FileInputStream input = new FileInputStream(filename);
                Properties props = new Properties();
                props.load(input);
                input.close();
                return (MBeanServerForwarder) Proxy.newProxyInstance(
                        MBeanServerForwarder.class.getClassLoader(),
                        new Class[] {MBeanServerForwarder.class},
                        new RoleBasedAuthorizationModule((Map) props));
            } else {
                throw new IllegalArgumentException("Unknown MBeanServerForwarder type: " + type);
            }
        } catch (Exception e) {
            throw new IllegalArgumentException("Failed creating the MBeanServerForwarder: " + e.getLocalizedMessage());
        }
    }

    /**
     * Gets a local reference to the MBeanServer and initializes the
     * authorization manager.
     *
     * This constructor must not be called, use the
     * {@link RoleBasedAuthorizationModule#newProxyInstance(String, String)}
     * static method instead.
     *
     * @param access  Access rights used by the module.
     */
    private RoleBasedAuthorizationModule(final Map<String, String> access) {
        for (Iterator<String> i = access.keySet().iterator(); i.hasNext();) {
            MethodType type;
            String key = i.next();
            String value = access.get(key);
            if ("readonly".equals(value)) {
                type = MethodType.GETTER;
            } else if ("readwrite".equals(value)) {
                type = MethodType.SETTER;
            } else {
                throw new IllegalArgumentException("Unknown access right: " + value
                        + ". Valid values are \"readonly\" and \"readwrite\".");
            }
            accessRights.put(key, type);
        }
    }

    /**
     * @param jmxService the jmxService to set
     */
    public void setJmxService(final JmxService jmxService) {
        mBeanServer = jmxService.getJmxServer();
    }

    /**
     * Verifies that the invoker has the right of invoking a certain method
     * and invokes it on the MBeanServer.
     *
     * @param  proxy   The proxy instance that the method was invoked on.
     * @param  method  Method to invoke.
     * @param  args    Arguments to give to the method.
     *
     * @return Result of the invocation.
     *
     * @throws IllegalAccessException     If the current user is not allowed to
     *                                    invoke the given method.
     * @throws Throwable                  If the target throws an exception.
     */
    public Object invoke(final Object proxy, final Method method, final Object[] args)
    throws IllegalAccessException, Throwable {
        // Check arguments
        if (method == null) {
            throw new IllegalArgumentException("Method is null");
        }
        String methodName = method.getName();
        if (methodName == null) {
            throw new IllegalArgumentException("MethodName is null");
        }

        // Get the caller's identification information
        Subject subject = Subject.getSubject(AccessController.getContext());

        // setMBeanServer is a special operation, only allowed when
        // no subject present (=local operation)
        if (subject == null) {
            if ("setMBeanServer".equals(methodName)) {
                mBeanServer = (MBeanServer) args[0];
                return null;
            }
        }

        // Check MBeanServer
        if (mBeanServer == null) {
            throw new IllegalStateException("MBeanServer is null");
        }

        // Local operations (=no subject present) are called directly
        if (subject == null) {
            return method.invoke(mBeanServer, args);
        }

        // Get method type
        MethodType type;
        if (methodName.startsWith("get") || methodName.startsWith("query")
                || methodName.startsWith("is") || methodName.startsWith("to")
                || "equals".equals(methodName) || "hashCode".equals(methodName)) {
            type = MethodType.GETTER;
        } else {
            // In all other cases assume setter
            type = MethodType.SETTER;
        }

        // Verify and invoke
        if (canAccess(type, getRoles(subject))) {
            try {
                return method.invoke(mBeanServer, args);
            } catch (InvocationTargetException e) {
                throw e.getTargetException();
            }
        } else {
            throw new IllegalAccessException("Access denied for method " + methodName);
        }
    }

    /**
     * Gets the roles of a given subject.
     *
     * @param subject  Subject to inspect.
     *
     * @return Set of roles.
     */
    private Set<String> getRoles(final Subject subject) {
        Set<String> roles = new HashSet<String>();
        // Retrieve all roles of the user
        // Roles are members of the Group.class
        for (Iterator<Group> i = subject.getPrincipals(Group.class).iterator(); i.hasNext();) {
            Group group = i.next();

            // Add all roles
            for (Enumeration e = group.members(); e.hasMoreElements();) {
                Principal p = (Principal) e.nextElement();
                roles.add(p.getName());
            }
        }
        return roles;
    }

    /**
     * Checks whether a set of roles are sufficient to access a given type of
     * method.
     *
     * @param type   Method type.
     * @param roles  Set of roles.
     *
     * @return true if roles can access that type of method, false otherwise.
     */
    private boolean canAccess(final MethodType type, final Set<String> roles) {
        if (type != MethodType.GETTER && type != MethodType.SETTER) {
            throw new IllegalArgumentException("Unknown method type: " + type);
        }

        // Get the largest set of authorization for those roles
        MethodType accessRight = accessRights.get("*");
        for (Iterator<String> iterator = roles.iterator(); iterator.hasNext() && accessRight != MethodType.SETTER;) {
            String role = iterator.next();
            MethodType currentAccessRight = accessRights.get(role);
            if ((currentAccessRight == MethodType.GETTER && accessRight == null)
                    || currentAccessRight == MethodType.SETTER) {
                accessRight = currentAccessRight;
            }
        }

        return (accessRight == type || accessRight == MethodType.SETTER);
    }

    /**
     * Local reference to the MBeanServer.
     */
    private MBeanServer mBeanServer = null;

    /**
     * Access rights.
     */
    private Map<String, MethodType> accessRights = new HashMap<String, MethodType>();

    /**
     * Method types.
     */
    private static enum MethodType {
        /**
         * A getter method.
         */
        GETTER,

        /**
         * A setter method.
         */
        SETTER
    }
}
