/*
* Copyright (c) 2003, 2008, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code 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 General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.jmx.remote.security;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.Principal;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Pattern;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.security.auth.Subject;
/**
* <p>An object of this class implements the MBeanServerAccessController
* interface and, for each of its methods, calls an appropriate checking
* method and then forwards the request to a wrapped MBeanServer object.
* The checking method may throw a SecurityException if the operation is
* not allowed; in this case the request is not forwarded to the
* wrapped object.</p>
*
* <p>This class implements the {@link #checkRead()}, {@link #checkWrite()},
* {@link #checkCreate(String)}, and {@link #checkUnregister(ObjectName)}
* methods based on an access level properties file containing username/access
* level pairs. The set of username/access level pairs is passed either as a
* filename which denotes a properties file on disk, or directly as an instance
* of the {@link Properties} class. In both cases, the name of each property
* represents a username, and the value of the property is the associated access
* level. Thus, any given username either does not exist in the properties or
* has exactly one access level. The same access level can be shared by several
* usernames.</p>
*
* <p>The supported access level values are {@code readonly} and
* {@code readwrite}. The {@code readwrite} access level can be
* qualified by one or more <i>clauses</i>, where each clause looks
* like <code>create <i>classNamePattern</i></code> or {@code
* unregister}. For example:</p>
*
* <pre>
* monitorRole readonly
* controlRole readwrite \
* create javax.management.timer.*,javax.management.monitor.* \
* unregister
* </pre>
*
* <p>(The continuation lines with {@code \} come from the parser for
* Properties files.)</p>
*/
public class MBeanServerFileAccessController
extends MBeanServerAccessController {
static final String READONLY = "readonly";
static final String READWRITE = "readwrite";
static final String CREATE = "create";
static final String UNREGISTER = "unregister";
private enum AccessType {READ, WRITE, CREATE, UNREGISTER};
private static class Access {
final boolean write;
final String[] createPatterns;
private boolean unregister;
Access(boolean write, boolean unregister, List<String> createPatternList) {
this.write = write;
int npats = (createPatternList == null) ? 0 : createPatternList.size();
if (npats == 0)
this.createPatterns = NO_STRINGS;
else
this.createPatterns = createPatternList.toArray(new String[npats]);
this.unregister = unregister;
}
private final String[] NO_STRINGS = new String[0];
}
/**
* <p>Create a new MBeanServerAccessController that forwards all the
* MBeanServer requests to the MBeanServer set by invoking the {@link
* #setMBeanServer} method after doing access checks based on read and
* write permissions.</p>
*
* <p>This instance is initialized from the specified properties file.</p>
*
* @param accessFileName name of the file which denotes a properties
* file on disk containing the username/access level entries.
*
* @exception IOException if the file does not exist, is a
* directory rather than a regular file, or for some other
* reason cannot be opened for reading.
*
* @exception IllegalArgumentException if any of the supplied access
* level values differs from "readonly" or "readwrite".
*/
public MBeanServerFileAccessController(String accessFileName)
throws IOException {
super();
this.accessFileName = accessFileName;
Properties props = propertiesFromFile(accessFileName);
parseProperties(props);
}
/**
* <p>Create a new MBeanServerAccessController that forwards all the
* MBeanServer requests to <code>mbs</code> after doing access checks
* based on read and write permissions.</p>
*
* <p>This instance is initialized from the specified properties file.</p>
*
* @param accessFileName name of the file which denotes a properties
* file on disk containing the username/access level entries.
*
* @param mbs the MBeanServer object to which requests will be forwarded.
*
* @exception IOException if the file does not exist, is a
* directory rather than a regular file, or for some other
* reason cannot be opened for reading.
*
* @exception IllegalArgumentException if any of the supplied access
* level values differs from "readonly" or "readwrite".
*/
public MBeanServerFileAccessController(String accessFileName,
MBeanServer mbs)
throws IOException {
this(accessFileName);
setMBeanServer(mbs);
}
/**
* <p>Create a new MBeanServerAccessController that forwards all the
* MBeanServer requests to the MBeanServer set by invoking the {@link
* #setMBeanServer} method after doing access checks based on read and
* write permissions.</p>
*
* <p>This instance is initialized from the specified properties
* instance. This constructor makes a copy of the properties
* instance and it is the copy that is consulted to check the
* username and access level of an incoming connection. The
* original properties object can be modified without affecting
* the copy. If the {@link #refresh} method is then called, the
* <code>MBeanServerFileAccessController</code> will make a new
* copy of the properties object at that time.</p>
*
* @param accessFileProps properties list containing the username/access
* level entries.
*
* @exception IllegalArgumentException if <code>accessFileProps</code> is
* <code>null</code> or if any of the supplied access level values differs
* from "readonly" or "readwrite".
*/
public MBeanServerFileAccessController(Properties accessFileProps)
throws IOException {
super();
if (accessFileProps == null)
throw new IllegalArgumentException("Null properties");
originalProps = accessFileProps;
parseProperties(accessFileProps);
}
/**
* <p>Create a new MBeanServerAccessController that forwards all the
* MBeanServer requests to the MBeanServer set by invoking the {@link
* #setMBeanServer} method after doing access checks based on read and
* write permissions.</p>
*
* <p>This instance is initialized from the specified properties
* instance. This constructor makes a copy of the properties
* instance and it is the copy that is consulted to check the
* username and access level of an incoming connection. The
* original properties object can be modified without affecting
* the copy. If the {@link #refresh} method is then called, the
* <code>MBeanServerFileAccessController</code> will make a new
* copy of the properties object at that time.</p>
*
* @param accessFileProps properties list containing the username/access
* level entries.
*
* @param mbs the MBeanServer object to which requests will be forwarded.
*
* @exception IllegalArgumentException if <code>accessFileProps</code> is
* <code>null</code> or if any of the supplied access level values differs
* from "readonly" or "readwrite".
*/
public MBeanServerFileAccessController(Properties accessFileProps,
MBeanServer mbs)
throws IOException {
this(accessFileProps);
setMBeanServer(mbs);
}
/**
* Check if the caller can do read operations. This method does
* nothing if so, otherwise throws SecurityException.
*/
@Override
public void checkRead() {
checkAccess(AccessType.READ, null);
}
/**
* Check if the caller can do write operations. This method does
* nothing if so, otherwise throws SecurityException.
*/
@Override
public void checkWrite() {
checkAccess(AccessType.WRITE, null);
}
/**
* Check if the caller can create MBeans or instances of the given class.
* This method does nothing if so, otherwise throws SecurityException.
*/
@Override
public void checkCreate(String className) {
checkAccess(AccessType.CREATE, className);
}
/**
* Check if the caller can do unregister operations. This method does
* nothing if so, otherwise throws SecurityException.
*/
@Override
public void checkUnregister(ObjectName name) {
checkAccess(AccessType.UNREGISTER, null);
}
/**
* <p>Refresh the set of username/access level entries.</p>
*
* <p>If this instance was created using the
* {@link #MBeanServerFileAccessController(String)} or
* {@link #MBeanServerFileAccessController(String,MBeanServer)}
* constructors to specify a file from which the entries are read,
* the file is re-read.</p>
*
* <p>If this instance was created using the
* {@link #MBeanServerFileAccessController(Properties)} or
* {@link #MBeanServerFileAccessController(Properties,MBeanServer)}
* constructors then a new copy of the <code>Properties</code> object
* is made.</p>
*
* @exception IOException if the file does not exist, is a
* directory rather than a regular file, or for some other
* reason cannot be opened for reading.
*
* @exception IllegalArgumentException if any of the supplied access
* level values differs from "readonly" or "readwrite".
*/
public synchronized void refresh() throws IOException {
Properties props;
if (accessFileName == null)
props = (Properties) originalProps;
else
props = propertiesFromFile(accessFileName);
parseProperties(props);
}
private static Properties propertiesFromFile(String fname)
throws IOException {
FileInputStream fin = new FileInputStream(fname);
try {
Properties p = new Properties();
p.load(fin);
return p;
} finally {
fin.close();
}
}
private synchronized void checkAccess(AccessType requiredAccess, String arg) {
final AccessControlContext acc = AccessController.getContext();
final Subject s =
AccessController.doPrivileged(new PrivilegedAction<Subject>() {
public Subject run() {
return Subject.getSubject(acc);
}
});
if (s == null) return; /* security has not been enabled */
final Set principals = s.getPrincipals();
String newPropertyValue = null;
for (Iterator i = principals.iterator(); i.hasNext(); ) {
final Principal p = (Principal) i.next();
Access access = accessMap.get(p.getName());
if (access != null) {
boolean ok;
switch (requiredAccess) {
case READ:
ok = true; // all access entries imply read
break;
case WRITE:
ok = access.write;
break;
case UNREGISTER:
ok = access.unregister;
if (!ok && access.write)
newPropertyValue = "unregister";
break;
case CREATE:
ok = checkCreateAccess(access, arg);
if (!ok && access.write)
newPropertyValue = "create " + arg;
break;
default:
throw new AssertionError();
}
if (ok)
return;
}
}
SecurityException se = new SecurityException("Access denied! Invalid " +
"access level for requested MBeanServer operation.");
// Add some more information to help people with deployments that
// worked before we required explicit create clauses. We're not giving
// any information to the bad guys, other than that the access control
// is based on a file, which they could have worked out from the stack
// trace anyway.
if (newPropertyValue != null) {
SecurityException se2 = new SecurityException("Access property " +
"for this identity should be similar to: " + READWRITE +
" " + newPropertyValue);
se.initCause(se2);
}
throw se;
}
private static boolean checkCreateAccess(Access access, String className) {
for (String classNamePattern : access.createPatterns) {
if (classNameMatch(classNamePattern, className))
return true;
}
return false;
}
private static boolean classNameMatch(String pattern, String className) {
// We studiously avoided regexes when parsing the properties file,
// because that is done whenever the VM is started with the
// appropriate -Dcom.sun.management options, even if nobody ever
// creates an MBean. We don't want to incur the overhead of loading
// all the regex code whenever those options are specified, but if we
// get as far as here then the VM is already running and somebody is
// doing the very unusual operation of remotely creating an MBean.
// Because that operation is so unusual, we don't try to optimize
// by hand-matching or by caching compiled Pattern objects.
StringBuilder sb = new StringBuilder();
StringTokenizer stok = new StringTokenizer(pattern, "*", true);
while (stok.hasMoreTokens()) {
String tok = stok.nextToken();
if (tok.equals("*"))
sb.append("[^.]*");
else
sb.append(Pattern.quote(tok));
}
return className.matches(sb.toString());
}
private void parseProperties(Properties props) {
this.accessMap = new HashMap<String, Access>();
for (Map.Entry<Object, Object> entry : props.entrySet()) {
String identity = (String) entry.getKey();
String accessString = (String) entry.getValue();
Access access = Parser.parseAccess(identity, accessString);
accessMap.put(identity, access);
}
}
private static class Parser {
private final static int EOS = -1; // pseudo-codepoint "end of string"
static {
assert !Character.isWhitespace(EOS);
}
private final String identity; // just for better error messages
private final String s; // the string we're parsing
private final int len; // s.length()
private int i;
private int c;
// At any point, either c is s.codePointAt(i), or i == len and
// c is EOS. We use int rather than char because it is conceivable
// (if unlikely) that a classname in a create clause might contain
// "supplementary characters", the ones that don't fit in the original
// 16 bits for Unicode.
private Parser(String identity, String s) {
this.identity = identity;
this.s = s;
this.len = s.length();
this.i = 0;
if (i < len)
this.c = s.codePointAt(i);
else
this.c = EOS;
}
static Access parseAccess(String identity, String s) {
return new Parser(identity, s).parseAccess();
}
private Access parseAccess() {
skipSpace();
String type = parseWord();
Access access;
if (type.equals(READONLY))
access = new Access(false, false, null);
else if (type.equals(READWRITE))
access = parseReadWrite();
else {
throw syntax("Expected " + READONLY + " or " + READWRITE +
": " + type);
}
if (c != EOS)
throw syntax("Extra text at end of line");
return access;
}
private Access parseReadWrite() {
List<String> createClasses = new ArrayList<String>();
boolean unregister = false;
while (true) {
skipSpace();
if (c == EOS)
break;
String type = parseWord();
if (type.equals(UNREGISTER))
unregister = true;
else if (type.equals(CREATE))
parseCreate(createClasses);
else
throw syntax("Unrecognized keyword " + type);
}
return new Access(true, unregister, createClasses);
}
private void parseCreate(List<String> createClasses) {
while (true) {
skipSpace();
createClasses.add(parseClassName());
skipSpace();
if (c == ',')
next();
else
break;
}
}
private String parseClassName() {
// We don't check that classname components begin with suitable
// characters (so we accept 1.2.3 for example). This means that
// there are only two states, which we can call dotOK and !dotOK
// according as a dot (.) is legal or not. Initially we're in
// !dotOK since a classname can't start with a dot; after a dot
// we're in !dotOK again; and after any other characters we're in
// dotOK. The classname is only accepted if we end in dotOK,
// so we reject an empty name or a name that ends with a dot.
final int start = i;
boolean dotOK = false;
while (true) {
if (c == '.') {
if (!dotOK)
throw syntax("Bad . in class name");
dotOK = false;
} else if (c == '*' || Character.isJavaIdentifierPart(c))
dotOK = true;
else
break;
next();
}
String className = s.substring(start, i);
if (!dotOK)
throw syntax("Bad class name " + className);
return className;
}
// Advance c and i to the next character, unless already at EOS.
private void next() {
if (c != EOS) {
i += Character.charCount(c);
if (i < len)
c = s.codePointAt(i);
else
c = EOS;
}
}
private void skipSpace() {
while (Character.isWhitespace(c))
next();
}
private String parseWord() {
skipSpace();
if (c == EOS)
throw syntax("Expected word at end of line");
final int start = i;
while (c != EOS && !Character.isWhitespace(c))
next();
String word = s.substring(start, i);
skipSpace();
return word;
}
private IllegalArgumentException syntax(String msg) {
return new IllegalArgumentException(
msg + " [" + identity + " " + s + "]");
}
}
private Map<String, Access> accessMap;
private Properties originalProps;
private String accessFileName;
}