544 lines · 21.1 KB · Java
RawDownload
/* * 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();            }

Lines 1–500 of 544.