/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2005 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
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.objectweb.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.objectweb.jonas.common.JProp;
import org.objectweb.jonas.jmx.JmxService;
import org.objectweb.jonas.security.auth.JSigned;
import org.objectweb.jonas.service.ServiceManager;
import org.objectweb.security.context.SecurityContext;
import org.objectweb.security.context.SecurityCurrent;

/**
 * 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(String type, 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(Map access) {
        for (Iterator i = access.keySet().iterator(); i.hasNext();) {
            Integer type;
            String key = (String) i.next();
            String value = (String) access.get(key);
            if ("readonly".equals(value)) {
                type = RoleBasedAuthorizationModule.GETTER;
            } else if ("readwrite".equals(value)) {
                type = RoleBasedAuthorizationModule.SETTER;
            } else {
                throw new IllegalArgumentException("Unknown access right: " + value
                        + ". Valid values are \"readonly\" and \"readwrite\".");
            }
            accessRights.put(key, type);
        }
        mBeanServer = ((JmxService) ServiceManager.getInstance().getJmxService()).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 InvocationTargetException  Problem with the invocation target.
     * @throws IllegalAccessException     If the current user is not allowed to
     *                                    invoke the given method.
     */
    public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException, InvocationTargetException {
        // 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
        Integer type;
        if (methodName.startsWith("get") || methodName.startsWith("query")
                || methodName.startsWith("is") || methodName.startsWith("to")
                || "equals".equals(methodName) || "hashCode".equals(methodName)) {
            type = RoleBasedAuthorizationModule.GETTER;
        } else {
            // In all other cases assume setter
            type = RoleBasedAuthorizationModule.SETTER;
        }

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

    /**
     * Gets the roles of a given subject. Will try to use the SecurityContext
     * if present, otherwise will get the roles from the subject.
     *
     * @param subject  Subject to inspect.
     *
     * @return Set of roles (String).
     */
    private Set getRoles(Subject subject) {
        Set roles = new HashSet();
        SecurityContext context = SecurityCurrent.getCurrent().getSecurityContext();
        if (context != null) {
            String[] rolesArray = context.getRoles();
            for (int i = 0; i < rolesArray.length; i++) {
                roles.add(rolesArray[i]);
            }
        } else {
            // Retrieve all roles of the user
            // Roles are members of the Group.class
            for (Iterator i = subject.getPrincipals(Group.class).iterator(); i.hasNext();) {
                Group group = (Group) i.next();

                // A signed group is empty group that contains a signature
                if (group instanceof JSigned) {
                    continue;
                }

                // 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, must be {@link AuthorizationProvider#GETTER}
     *               or {@link AuthorizationProvider#SETTER}.
     * @param roles  Set of roles (String).
     *
     * @return true if roles can access that type of method, false otherwise.
     */
    private boolean canAccess(Integer type, Set roles) {
        if (type != RoleBasedAuthorizationModule.GETTER && type != RoleBasedAuthorizationModule.SETTER) {
            throw new IllegalArgumentException("Unknown method type: " + type);
        }

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

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

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

    /**
     * Access rights.
     */
    private Map accessRights = new HashMap();

    /**
     * A getter function.
     */
    static private final Integer GETTER = new Integer(1);

    /**
     * A setter function.
     */
    static private final Integer SETTER = new Integer(2);
}
