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