1 /* 2 * Copyright (c) 2003, 2008, 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 Access(boolean write, boolean unregister, List<String> createPatternList)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 */ MBeanServerFileAccessController(String accessFileName)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 */ MBeanServerFileAccessController(String accessFileName, MBeanServer mbs)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 */ MBeanServerFileAccessController(Properties accessFileProps)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 */ MBeanServerFileAccessController(Properties accessFileProps, MBeanServer mbs)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 checkRead()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 checkWrite()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 checkCreate(String className)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 checkUnregister(ObjectName name)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 */ refresh()283 public synchronized void refresh() throws IOException { 284 Properties props; 285 if (accessFileName == null) 286 props = (Properties) originalProps; 287 else 288 props = propertiesFromFile(accessFileName); 289 parseProperties(props); 290 } 291 propertiesFromFile(String fname)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 checkAccess(AccessType requiredAccess, String arg)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 checkCreateAccess(Access access, String className)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 classNameMatch(String pattern, String className)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 parseProperties(Properties props)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 { Character.isWhitespace(EOS)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 Parser(String identity, String s)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 parseAccess(String identity, String s)428 static Access parseAccess(String identity, String s) { 429 return new Parser(identity, s).parseAccess(); 430 } 431 parseAccess()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 parseReadWrite()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 parseCreate(List<String> createClasses)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 parseClassName()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. next()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 skipSpace()518 private void skipSpace() { 519 while (Character.isWhitespace(c)) 520 next(); 521 } 522 parseWord()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 syntax(String msg)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 } 545