1 /*
   2  * Copyright (c) 2003, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.jmx.remote.security;
  27 
  28 import java.io.FileInputStream;
  29 import java.io.IOException;
  30 import java.security.AccessControlContext;
  31 import java.security.AccessController;
  32 import java.security.Principal;
  33 import java.security.PrivilegedAction;
  34 import java.util.ArrayList;
  35 import java.util.HashMap;
  36 import java.util.Iterator;
  37 import java.util.List;
  38 import java.util.Map;
  39 import java.util.Properties;
  40 import java.util.Set;
  41 import java.util.StringTokenizer;
  42 import java.util.regex.Pattern;
  43 import javax.management.MBeanServer;
  44 import javax.management.ObjectName;
  45 import javax.security.auth.Subject;
  46 
  47 /**
  48  * <p>An object of this class implements the MBeanServerAccessController
  49  * interface and, for each of its methods, calls an appropriate checking
  50  * method and then forwards the request to a wrapped MBeanServer object.
  51  * The checking method may throw a SecurityException if the operation is
  52  * not allowed; in this case the request is not forwarded to the
  53  * wrapped object.</p>
  54  *
  55  * <p>This class implements the {@link #checkRead()}, {@link #checkWrite()},
  56  * {@link #checkCreate(String)}, and {@link #checkUnregister(ObjectName)}
  57  * methods based on an access level properties file containing username/access
  58  * level pairs. The set of username/access level pairs is passed either as a
  59  * filename which denotes a properties file on disk, or directly as an instance
  60  * of the {@link Properties} class.  In both cases, the name of each property
  61  * represents a username, and the value of the property is the associated access
  62  * level.  Thus, any given username either does not exist in the properties or
  63  * has exactly one access level. The same access level can be shared by several
  64  * usernames.</p>
  65  *
  66  * <p>The supported access level values are {@code readonly} and
  67  * {@code readwrite}.  The {@code readwrite} access level can be
  68  * qualified by one or more <i>clauses</i>, where each clause looks
  69  * like <code>create <i>classNamePattern</i></code> or {@code
  70  * unregister}.  For example:</p>
  71  *
  72  * <pre>
  73  * monitorRole  readonly
  74  * controlRole  readwrite \
  75  *              create javax.management.timer.*,javax.management.monitor.* \
  76  *              unregister
  77  * </pre>
  78  *
  79  * <p>(The continuation lines with {@code \} come from the parser for
  80  * Properties files.)</p>
  81  */
  82 public class MBeanServerFileAccessController
  83     extends MBeanServerAccessController {
  84 
  85     static final String READONLY = "readonly";
  86     static final String READWRITE = "readwrite";
  87 
  88     static final String CREATE = "create";
  89     static final String UNREGISTER = "unregister";
  90 
  91     private enum AccessType {READ, WRITE, CREATE, UNREGISTER};
  92 
  93     private static class Access {
  94         final boolean write;
  95         final String[] createPatterns;
  96         private boolean unregister;
  97 
  98         Access(boolean write, boolean unregister, List<String> createPatternList) {
  99             this.write = write;
 100             int npats = (createPatternList == null) ? 0 : createPatternList.size();
 101             if (npats == 0)
 102                 this.createPatterns = NO_STRINGS;
 103             else
 104                 this.createPatterns = createPatternList.toArray(new String[npats]);
 105             this.unregister = unregister;
 106         }
 107 
 108         private final String[] NO_STRINGS = new String[0];
 109     }
 110 
 111     /**
 112      * <p>Create a new MBeanServerAccessController that forwards all the
 113      * MBeanServer requests to the MBeanServer set by invoking the {@link
 114      * #setMBeanServer} method after doing access checks based on read and
 115      * write permissions.</p>
 116      *
 117      * <p>This instance is initialized from the specified properties file.</p>
 118      *
 119      * @param accessFileName name of the file which denotes a properties
 120      * file on disk containing the username/access level entries.
 121      *
 122      * @exception IOException if the file does not exist, is a
 123      * directory rather than a regular file, or for some other
 124      * reason cannot be opened for reading.
 125      *
 126      * @exception IllegalArgumentException if any of the supplied access
 127      * level values differs from "readonly" or "readwrite".
 128      */
 129     public MBeanServerFileAccessController(String accessFileName)
 130         throws IOException {
 131         super();
 132         this.accessFileName = accessFileName;
 133         Properties props = propertiesFromFile(accessFileName);
 134         parseProperties(props);
 135     }
 136 
 137     /**
 138      * <p>Create a new MBeanServerAccessController that forwards all the
 139      * MBeanServer requests to <code>mbs</code> after doing access checks
 140      * based on read and write permissions.</p>
 141      *
 142      * <p>This instance is initialized from the specified properties file.</p>
 143      *
 144      * @param accessFileName name of the file which denotes a properties
 145      * file on disk containing the username/access level entries.
 146      *
 147      * @param mbs the MBeanServer object to which requests will be forwarded.
 148      *
 149      * @exception IOException if the file does not exist, is a
 150      * directory rather than a regular file, or for some other
 151      * reason cannot be opened for reading.
 152      *
 153      * @exception IllegalArgumentException if any of the supplied access
 154      * level values differs from "readonly" or "readwrite".
 155      */
 156     public MBeanServerFileAccessController(String accessFileName,
 157                                            MBeanServer mbs)
 158         throws IOException {
 159         this(accessFileName);
 160         setMBeanServer(mbs);
 161     }
 162 
 163     /**
 164      * <p>Create a new MBeanServerAccessController that forwards all the
 165      * MBeanServer requests to the MBeanServer set by invoking the {@link
 166      * #setMBeanServer} method after doing access checks based on read and
 167      * write permissions.</p>
 168      *
 169      * <p>This instance is initialized from the specified properties
 170      * instance.  This constructor makes a copy of the properties
 171      * instance and it is the copy that is consulted to check the
 172      * username and access level of an incoming connection. The
 173      * original properties object can be modified without affecting
 174      * the copy. If the {@link #refresh} method is then called, the
 175      * <code>MBeanServerFileAccessController</code> will make a new
 176      * copy of the properties object at that time.</p>
 177      *
 178      * @param accessFileProps properties list containing the username/access
 179      * level entries.
 180      *
 181      * @exception IllegalArgumentException if <code>accessFileProps</code> is
 182      * <code>null</code> or if any of the supplied access level values differs
 183      * from "readonly" or "readwrite".
 184      */
 185     public MBeanServerFileAccessController(Properties accessFileProps)
 186         throws IOException {
 187         super();
 188         if (accessFileProps == null)
 189             throw new IllegalArgumentException("Null properties");
 190         originalProps = accessFileProps;
 191         parseProperties(accessFileProps);
 192     }
 193 
 194     /**
 195      * <p>Create a new MBeanServerAccessController that forwards all the
 196      * MBeanServer requests to the MBeanServer set by invoking the {@link
 197      * #setMBeanServer} method after doing access checks based on read and
 198      * write permissions.</p>
 199      *
 200      * <p>This instance is initialized from the specified properties
 201      * instance.  This constructor makes a copy of the properties
 202      * instance and it is the copy that is consulted to check the
 203      * username and access level of an incoming connection. The
 204      * original properties object can be modified without affecting
 205      * the copy. If the {@link #refresh} method is then called, the
 206      * <code>MBeanServerFileAccessController</code> will make a new
 207      * copy of the properties object at that time.</p>
 208      *
 209      * @param accessFileProps properties list containing the username/access
 210      * level entries.
 211      *
 212      * @param mbs the MBeanServer object to which requests will be forwarded.
 213      *
 214      * @exception IllegalArgumentException if <code>accessFileProps</code> is
 215      * <code>null</code> or if any of the supplied access level values differs
 216      * from "readonly" or "readwrite".
 217      */
 218     public MBeanServerFileAccessController(Properties accessFileProps,
 219                                            MBeanServer mbs)
 220         throws IOException {
 221         this(accessFileProps);
 222         setMBeanServer(mbs);
 223     }
 224 
 225     /**
 226      * Check if the caller can do read operations. This method does
 227      * nothing if so, otherwise throws SecurityException.
 228      */
 229     @Override
 230     public void checkRead() {
 231         checkAccess(AccessType.READ, null);
 232     }
 233 
 234     /**
 235      * Check if the caller can do write operations.  This method does
 236      * nothing if so, otherwise throws SecurityException.
 237      */
 238     @Override
 239     public void checkWrite() {
 240         checkAccess(AccessType.WRITE, null);
 241     }
 242 
 243     /**
 244      * Check if the caller can create MBeans or instances of the given class.
 245      * This method does nothing if so, otherwise throws SecurityException.
 246      */
 247     @Override
 248     public void checkCreate(String className) {
 249         checkAccess(AccessType.CREATE, className);
 250     }
 251 
 252     /**
 253      * Check if the caller can do unregister operations.  This method does
 254      * nothing if so, otherwise throws SecurityException.
 255      */
 256     @Override
 257     public void checkUnregister(ObjectName name) {
 258         checkAccess(AccessType.UNREGISTER, null);
 259     }
 260 
 261     /**
 262      * <p>Refresh the set of username/access level entries.</p>
 263      *
 264      * <p>If this instance was created using the
 265      * {@link #MBeanServerFileAccessController(String)} or
 266      * {@link #MBeanServerFileAccessController(String,MBeanServer)}
 267      * constructors to specify a file from which the entries are read,
 268      * the file is re-read.</p>
 269      *
 270      * <p>If this instance was created using the
 271      * {@link #MBeanServerFileAccessController(Properties)} or
 272      * {@link #MBeanServerFileAccessController(Properties,MBeanServer)}
 273      * constructors then a new copy of the <code>Properties</code> object
 274      * is made.</p>
 275      *
 276      * @exception IOException if the file does not exist, is a
 277      * directory rather than a regular file, or for some other
 278      * reason cannot be opened for reading.
 279      *
 280      * @exception IllegalArgumentException if any of the supplied access
 281      * level values differs from "readonly" or "readwrite".
 282      */
 283     public synchronized void refresh() throws IOException {
 284         Properties props;
 285         if (accessFileName == null)
 286             props = originalProps;
 287         else
 288             props = propertiesFromFile(accessFileName);
 289         parseProperties(props);
 290     }
 291 
 292     private static Properties propertiesFromFile(String fname)
 293         throws IOException {
 294         FileInputStream fin = new FileInputStream(fname);
 295         try {
 296             Properties p = new Properties();
 297             p.load(fin);
 298             return p;
 299         } finally {
 300             fin.close();
 301         }
 302     }
 303 
 304     private synchronized void checkAccess(AccessType requiredAccess, String arg) {
 305         final AccessControlContext acc = AccessController.getContext();
 306         final Subject s =
 307             AccessController.doPrivileged(new PrivilegedAction<Subject>() {
 308                     public Subject run() {
 309                         return Subject.getSubject(acc);
 310                     }
 311                 });
 312         if (s == null) return; /* security has not been enabled */
 313         final Set principals = s.getPrincipals();
 314         String newPropertyValue = null;
 315         for (Iterator i = principals.iterator(); i.hasNext(); ) {
 316             final Principal p = (Principal) i.next();
 317             Access access = accessMap.get(p.getName());
 318             if (access != null) {
 319                 boolean ok;
 320                 switch (requiredAccess) {
 321                     case READ:
 322                         ok = true;  // all access entries imply read
 323                         break;
 324                     case WRITE:
 325                         ok = access.write;
 326                         break;
 327                     case UNREGISTER:
 328                         ok = access.unregister;
 329                         if (!ok && access.write)
 330                             newPropertyValue = "unregister";
 331                         break;
 332                     case CREATE:
 333                         ok = checkCreateAccess(access, arg);
 334                         if (!ok && access.write)
 335                             newPropertyValue = "create " + arg;
 336                         break;
 337                     default:
 338                         throw new AssertionError();
 339                 }
 340                 if (ok)
 341                     return;
 342             }
 343         }
 344         SecurityException se = new SecurityException("Access denied! Invalid " +
 345                 "access level for requested MBeanServer operation.");
 346         // Add some more information to help people with deployments that
 347         // worked before we required explicit create clauses. We're not giving
 348         // any information to the bad guys, other than that the access control
 349         // is based on a file, which they could have worked out from the stack
 350         // trace anyway.
 351         if (newPropertyValue != null) {
 352             SecurityException se2 = new SecurityException("Access property " +
 353                     "for this identity should be similar to: " + READWRITE +
 354                     " " + newPropertyValue);
 355             se.initCause(se2);
 356         }
 357         throw se;
 358     }
 359 
 360     private static boolean checkCreateAccess(Access access, String className) {
 361         for (String classNamePattern : access.createPatterns) {
 362             if (classNameMatch(classNamePattern, className))
 363                 return true;
 364         }
 365         return false;
 366     }
 367 
 368     private static boolean classNameMatch(String pattern, String className) {
 369         // We studiously avoided regexes when parsing the properties file,
 370         // because that is done whenever the VM is started with the
 371         // appropriate -Dcom.sun.management options, even if nobody ever
 372         // creates an MBean.  We don't want to incur the overhead of loading
 373         // all the regex code whenever those options are specified, but if we
 374         // get as far as here then the VM is already running and somebody is
 375         // doing the very unusual operation of remotely creating an MBean.
 376         // Because that operation is so unusual, we don't try to optimize
 377         // by hand-matching or by caching compiled Pattern objects.
 378         StringBuilder sb = new StringBuilder();
 379         StringTokenizer stok = new StringTokenizer(pattern, "*", true);
 380         while (stok.hasMoreTokens()) {
 381             String tok = stok.nextToken();
 382             if (tok.equals("*"))
 383                 sb.append("[^.]*");
 384             else
 385                 sb.append(Pattern.quote(tok));
 386         }
 387         return className.matches(sb.toString());
 388     }
 389 
 390     private void parseProperties(Properties props) {
 391         this.accessMap = new HashMap<String, Access>();
 392         for (Map.Entry<Object, Object> entry : props.entrySet()) {
 393             String identity = (String) entry.getKey();
 394             String accessString = (String) entry.getValue();
 395             Access access = Parser.parseAccess(identity, accessString);
 396             accessMap.put(identity, access);
 397         }
 398     }
 399 
 400     private static class Parser {
 401         private final static int EOS = -1;  // pseudo-codepoint "end of string"
 402         static {
 403             assert !Character.isWhitespace(EOS);
 404         }
 405 
 406         private final String identity;  // just for better error messages
 407         private final String s;  // the string we're parsing
 408         private final int len;   // s.length()
 409         private int i;
 410         private int c;
 411         // At any point, either c is s.codePointAt(i), or i == len and
 412         // c is EOS.  We use int rather than char because it is conceivable
 413         // (if unlikely) that a classname in a create clause might contain
 414         // "supplementary characters", the ones that don't fit in the original
 415         // 16 bits for Unicode.
 416 
 417         private Parser(String identity, String s) {
 418             this.identity = identity;
 419             this.s = s;
 420             this.len = s.length();
 421             this.i = 0;
 422             if (i < len)
 423                 this.c = s.codePointAt(i);
 424             else
 425                 this.c = EOS;
 426         }
 427 
 428         static Access parseAccess(String identity, String s) {
 429             return new Parser(identity, s).parseAccess();
 430         }
 431 
 432         private Access parseAccess() {
 433             skipSpace();
 434             String type = parseWord();
 435             Access access;
 436             if (type.equals(READONLY))
 437                 access = new Access(false, false, null);
 438             else if (type.equals(READWRITE))
 439                 access = parseReadWrite();
 440             else {
 441                 throw syntax("Expected " + READONLY + " or " + READWRITE +
 442                         ": " + type);
 443             }
 444             if (c != EOS)
 445                 throw syntax("Extra text at end of line");
 446             return access;
 447         }
 448 
 449         private Access parseReadWrite() {
 450             List<String> createClasses = new ArrayList<String>();
 451             boolean unregister = false;
 452             while (true) {
 453                 skipSpace();
 454                 if (c == EOS)
 455                     break;
 456                 String type = parseWord();
 457                 if (type.equals(UNREGISTER))
 458                     unregister = true;
 459                 else if (type.equals(CREATE))
 460                     parseCreate(createClasses);
 461                 else
 462                     throw syntax("Unrecognized keyword " + type);
 463             }
 464             return new Access(true, unregister, createClasses);
 465         }
 466 
 467         private void parseCreate(List<String> createClasses) {
 468             while (true) {
 469                 skipSpace();
 470                 createClasses.add(parseClassName());
 471                 skipSpace();
 472                 if (c == ',')
 473                     next();
 474                 else
 475                     break;
 476             }
 477         }
 478 
 479         private String parseClassName() {
 480             // We don't check that classname components begin with suitable
 481             // characters (so we accept 1.2.3 for example).  This means that
 482             // there are only two states, which we can call dotOK and !dotOK
 483             // according as a dot (.) is legal or not.  Initially we're in
 484             // !dotOK since a classname can't start with a dot; after a dot
 485             // we're in !dotOK again; and after any other characters we're in
 486             // dotOK.  The classname is only accepted if we end in dotOK,
 487             // so we reject an empty name or a name that ends with a dot.
 488             final int start = i;
 489             boolean dotOK = false;
 490             while (true) {
 491                 if (c == '.') {
 492                     if (!dotOK)
 493                         throw syntax("Bad . in class name");
 494                     dotOK = false;
 495                 } else if (c == '*' || Character.isJavaIdentifierPart(c))
 496                     dotOK = true;
 497                 else
 498                     break;
 499                 next();
 500             }
 501             String className = s.substring(start, i);
 502             if (!dotOK)
 503                 throw syntax("Bad class name " + className);
 504             return className;
 505         }
 506 
 507         // Advance c and i to the next character, unless already at EOS.
 508         private void next() {
 509             if (c != EOS) {
 510                 i += Character.charCount(c);
 511                 if (i < len)
 512                     c = s.codePointAt(i);
 513                 else
 514                     c = EOS;
 515             }
 516         }
 517 
 518         private void skipSpace() {
 519             while (Character.isWhitespace(c))
 520                 next();
 521         }
 522 
 523         private String parseWord() {
 524             skipSpace();
 525             if (c == EOS)
 526                 throw syntax("Expected word at end of line");
 527             final int start = i;
 528             while (c != EOS && !Character.isWhitespace(c))
 529                 next();
 530             String word = s.substring(start, i);
 531             skipSpace();
 532             return word;
 533         }
 534 
 535         private IllegalArgumentException syntax(String msg) {
 536             return new IllegalArgumentException(
 537                     msg + " [" + identity + " " + s + "]");
 538         }
 539     }
 540 
 541     private Map<String, Access> accessMap;
 542     private Properties originalProps;
 543     private String accessFileName;
 544 }