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