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

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.

* *

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.

* *

The supported access level values are {@code readonly} and * {@code readwrite}. The {@code readwrite} access level can be * qualified by one or more clauses, where each clause looks * like create classNamePattern or {@code * unregister}. For example:

* *
 * monitorRole  readonly
 * controlRole  readwrite \
 *              create javax.management.timer.*,javax.management.monitor.* \
 *              unregister
 * 
* *

(The continuation lines with {@code \} come from the parser for * Properties files.)

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

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.

* *

This instance is initialized from the specified properties file.

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

Create a new MBeanServerAccessController that forwards all the * MBeanServer requests to mbs after doing access checks * based on read and write permissions.

* *

This instance is initialized from the specified properties file.

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

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.

* *

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 * MBeanServerFileAccessController will make a new * copy of the properties object at that time.

* * @param accessFileProps properties list containing the username/access * level entries. * * @exception IllegalArgumentException if accessFileProps is * null 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); } /** *

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.

* *

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 * MBeanServerFileAccessController will make a new * copy of the properties object at that time.

* * @param accessFileProps properties list containing the username/access * level entries. * * @param mbs the MBeanServer object to which requests will be forwarded. * * @exception IllegalArgumentException if accessFileProps is * null 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); } /** *

Refresh the set of username/access level entries.

* *

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.

* *

If this instance was created using the * {@link #MBeanServerFileAccessController(Properties)} or * {@link #MBeanServerFileAccessController(Properties,MBeanServer)} * constructors then a new copy of the Properties object * is made.

* * @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() { 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(); for (Map.Entry 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 createClasses = new ArrayList(); 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 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 accessMap; private Properties originalProps; private String accessFileName; }