Wednesday, April 7, 2010

MarkUtils-JMX

Following my recent post on JMX Secure Connections / Avoiding Java System Properties, I am making another addition to MarkUtils: MarkUtils-JMX.

JMX Management Bean Metadata

My primary inspiration for this library was that JMX provides a generous amount of metadata along with each management bean, attribute, operation, and parameter - including names, descriptions, and impacts (INFO, ACTION_INFO, ACTION, or UNKNOWN) for operations. Parameter names must be provided through metadata, otherwise only the generated defaults of p0, p1, etc. are displayed. Unfortunately, associating this data with a management bean is currently a pain, and the only support available for this through the JDK itself is by subclassing StandardMBean, or completely instantiating a MBeanInfo yourself, and returning from a subclass of StandardMBean or from a custom DynamicMBean implementation.

MarkUtils-JMX provides a MBeanInfoBuilder class that serves as an alternative to the non-public classes used to create MBeanInfo's within the JDK. The most significant feature of MBeanInfoBuilder is support for including names, descriptions, and impacts that are read from Java annotations. The supported annotations are provided as part of this library in the com.ziesemer.utils.jmx.beanInfo package. Similar functionality is planned for release with Java 7 as part of JMX 2.0 / JSR 255, as detailed by Eamonn McManus in Playing with the JMX 2.0 API (2008-08-06, weblogs.java.net). Unfortunately, while I'm sure the annotations will not be compatible with my own (even just due to mine being in a com.ziesemer package), mine are available immediately, and support both Java 1.5 / 5.0 and Java 1.6 / 6.

MBeanInfoBuilder is not intended to be a complete replacement, as it currently doesn't support constructors, notifications, and descriptors, mostly as I currently have no use for them. However, these limitations are almost completely mitigated by MBeanInfoCombiner, which allows the metadata from a custom MBeanInfo built by MBeanInfoBuilder to supplement the default MBeanInfo built by the JDK's StandardMBean - effectively adding support for the custom annotations.

SimpleMBean, also included, is a alternative to the JDK's StandardMBean (as with MBeanInfoBuilder, and doesn't depend upon non-public classes. Along with utilizing MBeanInfoBuilder by default, this allows for easy extension / customization of the implementation. Additionally, both MBeanInfoBuilder and SimpleMBean are designed and written with performance as a primary focus. Performance is more of a concern for the actual MBean implementation than the building of the MBeanInfo, as the information should only have to be built once, where as the MBean implementation will likely be called repeatedly and often throughout the lifetime of the hosting application.

Authentication and Authorization

Also included in this library are classes to handle most of the work around securing JMX access by providing authentication and authorization.

Authentication is the simpler half, and is provided by com.ziesemer.utils.jmx.server.authentication.BaseJmxAuthenticator. This abstract class handles all the validation and breaking-down of the Object input parameter. An implementing class must only implement Subject authenticate(String username, String password). The authentication implementation is then registered as a value to the JMXConnectorServer.AUTHENTICATOR property, and passed-in as part of the environment map to JMXConnectorServerFactory.newJMXConnectorServer.

Authorization is a little more complex. Limited support is built-in to the JDK through Java's security policy and MBeanPermission. Unfortunately, the JDK approach seems overly-complex and did not meet several of my requirements. This library provides a high-performance and flexible alternative, BaseJmxAuthorizer.

BaseJmxAuthorizer and the other classes in com.ziesemer.utils.jmx.server.authorization are based on "Use Case 2" in Luis-Miguel Alventosa's blog post, Authentication and Authorization in JMX RMI connectors (2006-09-25, blogs.sun.com), where an InvocationHandler is used to selectively proxy requests to the management bean. Again, BaseJmxAuthorizer was designed and written with performance as a primary focus, along with flexibility. It is based on a Map (implemented with a HashMap), associating incoming method names (e.g. getAttribute or invoke) with an associated handler. This Map is publicly exposed, allowing for easy customization through modifications made to the map. Several default handlers, including the simple and unconditional AllowHandler and DenyHandler, are included in the package.

A ReadOnlyInvokeHandler is also provided, and allows all calls to operations / methods that are considered to be "read-only". The request impact (MBeanOperationInfo.getImpact()) is first consulted, and the request is allowed if INFO is returned. The request is otherwise denied, unless the impact returned is UNKNOWN, in which case an attempt to determine if the method has a "read-only" name is made, which checks for if the method name starts with "get", "is", "list", "query", or is equal to "hashCode". By default, it also allows for calls to ThreadMXBean.dumpAllThreads. These allowances allow for full and easy use of all "read-only" functionality provided by JConsole by default, without compromising on security.

Unfortunately, under Java 1.5 / 5.0, this currently denies access to get* methods on ThreadMXBean and LoggingMXBean, as these operations are marked as ACTION_INFO rather than UNKNOWN (or more preferably / correctly, INFO). This appears to have been somewhat fixed in Java 1.6 / 6, as per Sun Bug 6320104, it seems that UNKNOWN was simply not supported by the infrastructure in 1.5 / 5.0 at the time. A ReadOnlyJavaFixInvokeHandler is provided in the package as a work-around for use under Java 1.5 / 5.0. I reported Sun Bug 6933325 to address that these operations should now return INFO instead of ACTION_INFO, now that it is possible. Please view the Javadocs and/or source for additional details.

BaseJmxAuthorizer handles basic logging of method invocations through SLF4J by default.

This library's BaseJmxAuthorizer also simplifies authentication and improves performance by associating permissions with a subclass of JMXPrincipal, JmxPermissionPrincipal. This allows permissions to be obtained for a user once during authorization, without having to re-check permissions on each JMX call. Just be sure that this caching is accounted for, such that a user doesn't maintain unattended access if / once a permission is removed by the underlying security directory. A few options could be implementing a session timeout, or adding a call to a customized implementation to also remove the permissions from the runtime if / once removed from the user.

JmxPermissionPrincipal maintains an instance of a subclass of PermissionCollection, JmxPermissions, which contains one or more JmxPermission (subclass of Permission instances. Default provided JmxPermission's are CONNECT, READ_EX, INVOKE, and ADMIN. Other subclasses may be created and used, but should be kept as singletons. Please view the Javadocs and/or source for additional details.

Packaging with Apache Maven, Java versions

As with all my other MarkUtils libraries, MarkUtils-JMX is configured, compiled, tested, and packaged using Apache Maven. This library posed a little bit of a challenge, however, as I wanted to provide 2 versions, one built for Java 1.5 / 5.0, and one for Java 1.6 / 6. This is one of the few times I wish that Java had standard support for conditional compilation directives, such as those supported by the C preprocessor.

The primary reason for requiring dual versions is for Java 6's support of MXBean's, which were not supported in Java 5. This includes use of the additional 2-arg constructor added to StandardMBean, which as far as I can see, is impossible to implement without being available at compile-time - even if considering reflection tricks.

The solution I decided on for now is to offer dual packages / builds: com.ziesemer.utils.jmx.java5 and com.ziesemer.utils.jmx.java6. The Java 6 version includes the source paths from the Java 5 version. To clarify, only the Java 5 or the Java 6 version is required, as the Java 6 version is a superset of the Java 5 version. The Java 5 version includes a StandardMBeanInfoCombiner5 class that is a subclass of StandardMBean and utilizes MBeanInfoBuilder and MBeanInfoCombiner. The Java 6 version includes a StandardMBeanInfoCombiner6, which is the same as the Java 5 version, but exposes the added constructor and allows use of MXBeans. This setup actually worked with Maven fairly well, including the ability to automatically run Maven builds against both versions in one operation, and including packaging the build outputs together into the final distribution.

The Java 6 version also includes a DefaultRegistration class, which registers a few additional custom MXBeans that I found helpful and that provide functionality not currently offered by the MXBeans provided by the JDK (ManagementFactory). These include my custom CharsetMXBean, SecurityMXBean, and SystemMXBean. (As these custom beans themselves don't require any Java 6-specific features or calls at compile time, they are actually included in the Java 5 package. They just require Java 6 to be registered as MXBeans rather than regular MBeans.)

Download

com.ziesemer.utils.jmx is available on ziesemer.dev.java.net under the GPL license, complete with source code, a compiled .jar, generated JavaDocs, and a suite of 40+ JUnit tests. Download the com.ziesemer.utils.jmx-*.zip distribution from here. Please report any bugs or feature requests on the java.net Issue Tracker.

No comments: