BaseJmxAuthorizer.java

package com.ziesemer.utils.jmx.server.authorization;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.management.MBeanServer;
import javax.management.remote.JMXPrincipal;
import javax.management.remote.MBeanServerForwarder;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <p>By default, only allows the execution of the method names defined in
 * 		{@link #READ_ONLY_MBEAN_SERVER_METHOD_NAMES}, and
 * 		{@link MBeanServer#invoke(javax.management.ObjectName, String, Object[], String[])}
 * 		using {@link ReadOnlyInvokeHandler}.</p>
 * <p>Most common customizations will involve setting new handlers on a per-method basis by
 * 		editing the {@link Map} returned by {@link #getMethodHandlers()}.
 * 	If a method is not defined in the map, {@link #getDefaultHandler()} is used instead,
 * 		and may be set using {@link #setDefaultHandler(IJmxInvocationAuthorizer)}.</p>
 * <p>Based on <a href="http://blogs.sun.com/lmalventosa/entry/jmx_authentication_authorization"
 * 		>http://blogs.sun.com/lmalventosa/entry/jmx_authentication_authorization</a>.</p>
 * @author Mark A. Ziesemer
 * 	<a href="http://www.ziesemer.com.">&lt;www.ziesemer.com&gt;</a>
 */
public class BaseJmxAuthorizer implements InvocationHandler{
	
	protected final Logger logger = LoggerFactory.getLogger(BaseJmxAuthorizer.class);
	
	protected static final Set<String> READ_ONLY_MBEAN_SERVER_METHOD_NAMES
		= Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(
			"getAttribute", "getAttributes",
			"getDefaultDomain", "getDomains", "getMBeanCount", "getMBeanInfo",
			"hashCode", "isInstanceOf", "isRegistered", "queryNames")));
	
	protected MBeanServer mBeanServer;
	protected final Map<String, IJmxInvocationAuthorizer> methodHandlers
		= new HashMap<String, IJmxInvocationAuthorizer>(32);
	/**
	 * Must never be null.
	 */
	private IJmxInvocationAuthorizer defaultHandler = new DenyHandler("default");
	
	public MBeanServerForwarder asMBeanServerForwarder(){
		final Object proxy = Proxy.newProxyInstance(
			MBeanServerForwarder.class.getClassLoader(),
			new Class[]{MBeanServerForwarder.class},
			this);
		return MBeanServerForwarder.class.cast(proxy);
	}
	
	public BaseJmxAuthorizer(){
		for(final String readOnlyMethodName : READ_ONLY_MBEAN_SERVER_METHOD_NAMES){
			methodHandlers.put(readOnlyMethodName, AllowHandler.INSTANCE);
		}
		methodHandlers.put("getMBeanServer", new GetMBeanServerHandler());
		methodHandlers.put("setMBeanServer", new SetMBeanServerHandler());
		
		final DenyHandler mBeanModificationHandler = new DenyHandler("MBean modification");
		methodHandlers.put("createMBean", mBeanModificationHandler);
		methodHandlers.put("unregisterMBean", mBeanModificationHandler);
		
		methodHandlers.put("invoke", new ReadOnlyInvokeHandler());
		
		final ServerSelfAllowHandler serverSelfAllowHandler = new ServerSelfAllowHandler();
		methodHandlers.put("getClassLoader", serverSelfAllowHandler);
		methodHandlers.put("getClassLoaderFor", serverSelfAllowHandler);
		methodHandlers.put("getClassLoaderRepository", serverSelfAllowHandler);
		methodHandlers.put("addNotificationListener", serverSelfAllowHandler);
		methodHandlers.put("removeNotificationListener", serverSelfAllowHandler);
	}
	
	public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable{
		final JmxAuthorizeContext ac = new JmxAuthorizeContext(mBeanServer, proxy, method, args);
		return invoke(ac);
	}
	
	/**
	 * <p>This method is designed to be overridden by child classes.
	 * 	A common practice may be to call <code>super</code>, surrounded with
	 * 		calls that make use of the {@link JmxAuthorizeContext}.</p>
	 */
	protected Object invoke(final JmxAuthorizeContext ac) throws Throwable{
		try{
			IJmxInvocationAuthorizer ia = methodHandlers.get(ac.method.getName());
			if(ia == null){
				ia = defaultHandler;
			}
			return ia.invoke(ac);
		}catch(final SecurityException se){
			final JMXPrincipal p = ac.getPrincipal();
			final Object[] args = ac.args;
			logger.warn("{}: method: {}, identity: {}, args: {}",
				se.getMessage(), ac.method.getName(),
				p != null ? p.getName() : null,
				args != null ? Arrays.asList(args) : null);
			throw se;
		}
	}
	
	protected Object logAndExecute(final JmxAuthorizeContext ac) throws Throwable{
		logExecution(ac);
		return ac.invoke();
	}
	
	protected void logExecution(final JmxAuthorizeContext ac){
		final JMXPrincipal p = ac.getPrincipal();
		final Object[] args = ac.args;
		logger.debug("Executing: {}, identity: {}, args: {}",
			ac.method.getName(),
			p != null ? p.getName() : null,
			args != null ? Arrays.asList(args) : null);
	}
	
	public MBeanServer getMBeanServer(){
		return mBeanServer;
	}
	
	public Map<String, IJmxInvocationAuthorizer> getMethodHandlers(){
		return methodHandlers;
	}
	
	public IJmxInvocationAuthorizer getDefaultHandler(){
		return defaultHandler;
	}
	public void setDefaultHandler(final IJmxInvocationAuthorizer defaultHandler){
		if(defaultHandler == null){
			throw new IllegalArgumentException("defaultHandler must not be null.");
		}
		this.defaultHandler = defaultHandler;
	}
	
	protected static class GetMBeanServerHandler implements IJmxInvocationAuthorizer{
		public Object invoke(final JmxAuthorizeContext ac) throws Throwable{
			return ac.mBeanServer;
		}
	}
	
	protected class SetMBeanServerHandler implements IJmxInvocationAuthorizer{
		public Object invoke(final JmxAuthorizeContext ac) throws Throwable{
			final Object[] args = ac.args;
			if(args == null || args.length < 1 || args[0] == null){
				throw new IllegalArgumentException("No MBeanServer specified.");
			}
			if(mBeanServer != null){
				throw new IllegalStateException("MBeanServer already initialized.");
			}
			mBeanServer = (MBeanServer)args[0];
			return null;
		}
	}
	
	protected static class ServerSelfAllowHandler implements IJmxInvocationAuthorizer{
		public Object invoke(final JmxAuthorizeContext ac) throws Throwable{
			if(ac.getSubject() == null){
				return ac.invoke();
			}
			throw new SecurityException("Access denied (must be server)");
		}
	}
	
	protected static class ImpactAndSig{
		public int impact;
		public String[] signature;
	}
	
}