/* | 
|
 * 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;  | 
|
}  |