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