View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.jexl3.introspection;
19  
20  import java.util.HashMap;
21  import java.util.HashSet;
22  import java.util.Map;
23  import java.util.Objects;
24  import java.util.Set;
25  import java.util.concurrent.ConcurrentHashMap;
26  
27  /**
28   * A sandbox describes permissions on a class by explicitly allowing or forbidding
29   * access to methods and properties through "allowlists" and "blocklists".
30   *
31   * <p>A <strong>allowlist</strong> explicitly allows methods/properties for a class;</p>
32   *
33   * <ul>
34   *   <li>If a allowlist is empty and thus does not contain any names,
35   *       all properties/methods are allowed for its class.</li>
36   *   <li>If it is not empty, the only allowed properties/methods are the ones contained.</li>
37   * </ul>
38   *
39   * <p>A <strong>blocklist</strong> explicitly forbids methods/properties for a class;</p>
40   *
41   * <ul>
42   *   <li>If a blocklist is empty and thus does not contain any names,
43   *       all properties/methods are forbidden for its class.</li>
44   *   <li>If it is not empty, the only forbidden properties/methods are the ones contained.</li>
45   * </ul>
46   *
47   * <p>Permissions are composed of three lists, read, write, execute, each being
48   * "allow" or "block":</p>
49   *
50   * <ul>
51   *   <li><strong>read</strong> controls readable properties </li>
52   *   <li><strong>write</strong> controls writable properties</li>
53   *   <li><strong>execute</strong> controls executable methods and constructor</li>
54   * </ul>
55   *
56   * <p>When specified, permissions - allow or block lists - can be created inheritable
57   * on interfaces or classes and thus applicable to their implementations or derived
58   * classes; the sandbox must be created with the 'inheritable' flag for this behavior
59   * to be triggered. Note that even in this configuration, it is still possible to
60   * add non-inheritable permissions.
61   * Adding inheritable lists to a non inheritable sandbox has no added effect;
62   * permissions only apply to their specified class.</p>
63   *
64   * <p>Note that a JexlUberspect always uses a <em>copy</em> of the JexlSandbox
65   * used to built it preventing permission changes after its instantiation.</p>
66   *
67   * @since 3.0
68   */
69  public final class JexlSandbox {
70      /**
71       * The marker string for explicitly disallowed null properties.
72       */
73      public static final String NULL = "?";
74  
75      /**
76       * The pass-thru name set.
77       */
78      static final Names ALLOW_NAMES = new Names() {
79          @Override
80          public boolean add(final String name) {
81              return false;
82          }
83  
84          @Override
85          public String toString() {
86              return "allowAll";
87          }
88      };
89  
90      /**
91       * The block-all name set.
92       */
93      private static final Names BLOCK_NAMES = new Names() {
94          @Override
95          public boolean add(final String name) {
96              return false;
97          }
98  
99          @Override
100         public String get(final String name) {
101             return name == null ? NULL : null;
102         }
103 
104         @Override
105         public String toString() {
106             return "blockAll";
107         }
108     };
109 
110     /**
111      * The block-all permissions.
112      */
113     private static final Permissions BLOCK_ALL = new Permissions(false, BLOCK_NAMES, BLOCK_NAMES, BLOCK_NAMES);
114     /**
115      * The pass-thru permissions.
116      */
117     private static final Permissions ALLOW_ALL = new Permissions(false, ALLOW_NAMES, ALLOW_NAMES, ALLOW_NAMES);
118     /**
119      * The map from class names to permissions.
120      */
121     private final Map<String, Permissions> sandbox;
122     /**
123      * Whether permissions can be inherited (through implementation or extension).
124      */
125     private final boolean inherit;
126     /**
127      * Default behavior, block or allow.
128      */
129     private final boolean allow;
130 
131     /**
132      * Creates a new default sandbox.
133      * <p>In the absence of explicit permissions on a class, the
134      * sandbox is an allow-box, allow-listing that class for all permissions (read, write and execute).
135      */
136     public JexlSandbox() {
137         this(true, false, null);
138     }
139 
140     /**
141      * Creates a new default sandbox.
142      * <p>A allow-box considers no permissions as &quot;everything is allowed&quot; when
143      * a block-box considers no permissions as &quot;nothing is allowed&quot;.
144      *
145      * @param ab whether this sandbox is allow (true) or block (false)
146      *           if no permission is explicitly defined for a class.
147      * @since 3.1
148      */
149     public JexlSandbox(final boolean ab) {
150         this(ab, false, null);
151     }
152 
153     /**
154      * Creates a sandbox.
155      *
156      * @param ab  whether this sandbox is allow (true) or block (false)
157      * @param inh whether permissions on interfaces and classes are inherited (true) or not (false)
158      * @since 3.2
159      */
160     public JexlSandbox(final boolean ab, final boolean inh) {
161         this(ab, inh, null);
162     }
163 
164     /**
165      * Creates a sandbox based on an existing permissions map.
166      *
167      * @param ab  whether this sandbox is allow (true) or block (false)
168      * @param inh whether permissions are inherited, default false
169      * @param map the permissions map
170      * @since 3.2
171      */
172     private JexlSandbox(final boolean ab, final boolean inh, final Map<String, Permissions> map) {
173         allow = ab;
174         inherit = inh;
175         sandbox = map != null ? map : new HashMap<>();
176     }
177 
178     /**
179      * Gets a class by name, crude mechanism for backwards (&lt;3.2 ) compatibility.
180      *
181      * @param cname the class name
182      * @return the class
183      */
184     static Class<?> forName(final String cname) {
185         try {
186             return Class.forName(cname);
187         } catch (final Exception xany) {
188             return null;
189         }
190     }
191 
192     /**
193      * Creates a new set of permissions based on allow lists for methods and properties for a given class.
194      * <p>The sandbox inheritance property will apply to the permissions created by this method
195      *
196      * @param clazz the allowed class name
197      * @return the permissions instance
198      */
199     public Permissions allow(final String clazz) {
200         return permissions(clazz, true, true, true);
201     }
202 
203     /**
204      * Creates a new set of permissions based on block lists for methods and properties for a given class.
205      * <p>The sandbox inheritance property will apply to the permissions created by this method
206      *
207      * @param clazz the blocked class name
208      * @return the permissions instance
209      */
210     public Permissions block(final String clazz) {
211         return permissions(clazz, false, false, false);
212     }
213 
214     /**
215      * Gets a copy of this sandbox
216      * @return a copy of this sandbox
217      */
218     public JexlSandbox copy() {
219         // modified concurrently at runtime so...
220         final Map<String, Permissions> map = new ConcurrentHashMap<>();
221         for (final Map.Entry<String, Permissions> entry : sandbox.entrySet()) {
222             map.put(entry.getKey(), entry.getValue().copy());
223         }
224         return new JexlSandbox(allow, inherit, map);
225     }
226 
227     /**
228      * Gets the execute permission value for a given method of a class.
229      *
230      * @param clazz the class
231      * @param name  the method name
232      * @return null if not allowed, the name of the method to use otherwise
233      */
234     public String execute(final Class<?> clazz, final String name) {
235         final String m = get(clazz).execute().get(name);
236         return "".equals(name) && m != null ? clazz.getName() : m;
237     }
238 
239     /**
240      * Gets the execute permission value for a given method of a class.
241      *
242      * @param clazz the class name
243      * @param name  the method name
244      * @return null if not allowed, the name of the method to use otherwise
245      * @deprecated 3.3
246      */
247     @Deprecated
248     public String execute(final String clazz, final String name) {
249         final String m = get(clazz).execute().get(name);
250         return "".equals(name) && m != null ? clazz : m;
251     }
252 
253     /**
254      * Gets the set of permissions associated to a class.
255      *
256      * @param clazz the class name
257      * @return the defined permissions or an all-allow permission instance if none were defined
258      */
259     public Permissions get(final String clazz) {
260         return get(forName(clazz));
261     }
262 
263     /**
264      * Gets the permissions associated to a class.
265      *
266      * @param clazz the class
267      * @return the permissions
268      */
269     @SuppressWarnings("null")
270     public Permissions get(final Class<?> clazz) {
271         // argument clazz cannot be null since permissions would be not null and block:
272         // we only store the result for classes we actively seek permissions for.
273         return compute(clazz, true);
274     }
275 
276     private static Permissions inheritable(final Permissions p) {
277         return p != null && p.isInheritable() ? p : null;
278     }
279 
280     /**
281      * Computes and optionally stores the permissions associated to a class.
282      *
283      * @param clazz the class
284      * @param store whether the computed permissions should be stored in the sandbox
285      * @return the permissions
286      */
287     private Permissions compute(final Class<?> clazz, final boolean store) {
288         // belt and suspender; recursion should not lead here
289         if (clazz == null) {
290             return BLOCK_ALL;
291         }
292         final String className = clazz.getName();
293         Permissions permissions = sandbox.get(className);
294         if (permissions == null) {
295             if (inherit) {
296                 // find first inherited interface that defines permissions
297                 final Class<?>[] interfaces = clazz.getInterfaces();
298                 for (int i = 0; permissions == null && i < interfaces.length; ++i) {
299                     permissions = inheritable(compute(interfaces[i], false));
300                 }
301                 // nothing defined yet, find first superclass that defines permissions
302                 if (permissions == null) {
303                     // let's recurse on super classes
304                     final Class<?> superClazz = clazz.getSuperclass();
305                     if (Object.class != superClazz) {
306                         permissions = inheritable(compute(superClazz, false));
307                     }
308                 }
309             }
310             // nothing was inheritable
311             if (permissions == null) {
312                 permissions = allow ? ALLOW_ALL : BLOCK_ALL;
313             }
314             // store the info to avoid doing this costly look-up
315             if (store) {
316                 sandbox.put(className, permissions);
317             }
318         }
319         return permissions;
320     }
321 
322     /**
323      * Creates the set of permissions for a given class.
324      * <p>The sandbox inheritance property will apply to the permissions created by this method
325      *
326      * @param clazz       the class for which these permissions apply
327      * @param readFlag    whether the readable property list is allow - true - or block - false -
328      * @param writeFlag   whether the writable property list is allow - true - or block - false -
329      * @param executeFlag whether the executable method list is allow - true - or block - false -
330      * @return the set of permissions
331      */
332     public Permissions permissions(final String clazz,
333                                    final boolean readFlag,
334                                    final boolean writeFlag,
335                                    final boolean executeFlag) {
336         return permissions(clazz, inherit, readFlag, writeFlag, executeFlag);
337     }
338 
339     /**
340      * Creates the set of permissions for a given class.
341      *
342      * @param clazz  the class for which these permissions apply
343      * @param inhf   whether these permissions are inheritable
344      * @param readf  whether the readable property list is allow - true - or block - false -
345      * @param writef whether the writable property list is allow - true - or block - false -
346      * @param execf  whether the executable method list is allow - true - or block - false -
347      * @return the set of permissions
348      */
349     public Permissions permissions(final String clazz,
350                                    final boolean inhf,
351                                    final boolean readf,
352                                    final boolean writef,
353                                    final boolean execf) {
354         final Permissions box = new Permissions(inhf, readf, writef, execf);
355         sandbox.put(clazz, box);
356         return box;
357     }
358 
359     /**
360      * Gets the read permission value for a given property of a class.
361      *
362      * @param clazz the class
363      * @param name  the property name
364      * @return null (or NULL if name is null) if not allowed, the name of the property to use otherwise
365      */
366     public String read(final Class<?> clazz, final String name) {
367         return get(clazz).read().get(name);
368     }
369 
370     /**
371      * Gets the write permission value for a given property of a class.
372      *
373      * @param clazz the class
374      * @param name  the property name
375      * @return null (or NULL if name is null) if not allowed, the name of the property to use otherwise
376      */
377     public String write(final Class<?> clazz, final String name) {
378         return get(clazz).write().get(name);
379     }
380 
381     /**
382      * Gets the write permission value for a given property of a class.
383      *
384      * @param clazz the class name
385      * @param name  the property name
386      * @return null if not allowed, the name of the property to use otherwise
387      * @deprecated 3.3
388      */
389     @Deprecated
390     public String write(final String clazz, final String name) {
391         return get(clazz).write().get(name);
392     }
393 
394     /**
395      * An allow set of names.
396      */
397     static class AllowSet extends Names {
398         /**
399          * The map of controlled names and aliases.
400          */
401         private Map<String, String> names;
402 
403         @Override
404         public boolean add(final String name) {
405             if (names == null) {
406                 names = new HashMap<>();
407             }
408             return names.put(name, name) == null;
409         }
410 
411         @Override
412         public boolean alias(final String name, final String alias) {
413             if (names == null) {
414                 names = new HashMap<>();
415             }
416             return names.put(alias, name) == null;
417         }
418 
419         @Override
420         protected Names copy() {
421             final AllowSet copy = new AllowSet();
422             copy.names = names == null ? null : new HashMap<>(names);
423             return copy;
424         }
425 
426         @Override
427         public String get(final String name) {
428             if (names == null) {
429                 return name;
430             }
431             final String actual = names.get(name);
432             // if null is not explicitly allowed, explicit null aka NULL
433             if (name == null && actual == null && !names.containsKey(null)) {
434                 return JexlSandbox.NULL;
435             }
436             return actual;
437         }
438 
439         @Override
440         public String toString() {
441             return "allow{" + (names == null ? "all" : Objects.toString(names.entrySet())) + "}";
442         }
443     }
444 
445     /**
446      * A block set of names.
447      */
448     static class BlockSet extends Names {
449         /**
450          * The set of controlled names.
451          */
452         private Set<String> names;
453 
454         @Override
455         public boolean add(final String name) {
456             if (names == null) {
457                 names = new HashSet<>();
458             }
459             return names.add(name);
460         }
461 
462         @Override
463         protected Names copy() {
464             final BlockSet copy = new BlockSet();
465             copy.names = names == null ? null : new HashSet<>(names);
466             return copy;
467         }
468 
469         @Override
470         public String get(final String name) {
471             // if name is null and contained in set, explicit null aka NULL
472             if (names != null && !names.contains(name)) {
473                 return name;
474             }
475             if (name != null) {
476                 return null;
477             }
478             return NULL;
479         }
480 
481         @Override
482         public String toString() {
483             return "block{" + (names == null ? "all" : Objects.toString(names)) + "}";
484         }
485     }
486 
487     /**
488      * A base set of names.
489      */
490     public abstract static class Names {
491 
492         /** Default constructor */
493         public Names() {} // Keep Javadoc happy
494 
495         /**
496          * Adds a name to this set.
497          *
498          * @param name the name to add
499          * @return true if the name was really added, false if not
500          */
501         public abstract boolean add(String name);
502 
503         /**
504          * Adds an alias to a name to this set.
505          * <p>This only has an effect on allow lists.</p>
506          *
507          * @param name  the name to alias
508          * @param alias the alias
509          * @return true if the alias was added, false if it was already present
510          */
511         public boolean alias(final String name, final String alias) {
512             return false;
513         }
514 
515         /**
516          * Gets a copy of these Names
517          * @return a copy of these Names
518          */
519         protected Names copy() {
520             return this;
521         }
522 
523         /**
524          * Gets whether a given name is allowed or not.
525          *
526          * @param name the method/property name to check
527          * @return null (or NULL if name is null) if not allowed, the actual name to use otherwise
528          */
529         public String get(final String name) {
530             return name;
531         }
532     }
533 
534     /**
535      * Contains the allow or block lists for properties and methods for a given class.
536      */
537     public static final class Permissions {
538         /**
539          * Whether these permissions are inheritable, ie can be used by derived classes.
540          */
541         private final boolean inheritable;
542         /**
543          * The controlled readable properties.
544          */
545         private final Names read;
546         /**
547          * The controlled  writable properties.
548          */
549         private final Names write;
550         /**
551          * The controlled methods.
552          */
553         private final Names execute;
554 
555         /**
556          * Creates a new permissions instance.
557          *
558          * @param inherit     whether these permissions are inheritable
559          * @param readFlag    whether the read property list is allow or block
560          * @param writeFlag   whether the write property list is allow or block
561          * @param executeFlag whether the method list is allow of block
562          */
563         Permissions(final boolean inherit, final boolean readFlag, final boolean writeFlag, final boolean executeFlag) {
564             this(inherit,
565                 readFlag ? new AllowSet() : new BlockSet(),
566                 writeFlag ? new AllowSet() : new BlockSet(),
567                 executeFlag ? new AllowSet() : new BlockSet());
568         }
569 
570         /**
571          * Creates a new permissions instance.
572          *
573          * @param inherit  whether these permissions are inheritable
574          * @param nread    the read set
575          * @param nwrite   the write set
576          * @param nexecute the method set
577          */
578         Permissions(final boolean inherit, final Names nread, final Names nwrite, final Names nexecute) {
579             this.read = nread != null ? nread : ALLOW_NAMES;
580             this.write = nwrite != null ? nwrite : ALLOW_NAMES;
581             this.execute = nexecute != null ? nexecute : ALLOW_NAMES;
582             this.inheritable = inherit;
583         }
584 
585         /**
586          * @return a copy of these permissions
587          */
588         Permissions copy() {
589             return new Permissions(inheritable, read.copy(), write.copy(), execute.copy());
590         }
591 
592         /**
593          * Gets the set of method names in these permissions.
594          *
595          * @return the set of method names
596          */
597         public Names execute() {
598             return execute;
599         }
600 
601         /**
602          * Adds a list of executable methods names to these permissions.
603          * <p>The constructor is denoted as the empty-string, all other methods by their names.</p>
604          *
605          * @param methodNames the method names
606          * @return this instance of permissions
607          */
608         public Permissions execute(final String... methodNames) {
609             for (final String methodName : methodNames) {
610                 execute.add(methodName);
611             }
612             return this;
613         }
614 
615         /**
616          * Do these permissions apply to derived classes?
617          * @return whether these permissions apply to derived classes.
618          */
619         public boolean isInheritable() {
620             return inheritable;
621         }
622 
623         /**
624          * Gets the set of readable property names in these permissions.
625          *
626          * @return the set of property names
627          */
628         public Names read() {
629             return read;
630         }
631 
632         /**
633          * Adds a list of readable property names to these permissions.
634          *
635          * @param propertyNames the property names
636          * @return this instance of permissions
637          */
638         public Permissions read(final String... propertyNames) {
639             for (final String propertyName : propertyNames) {
640                 read.add(propertyName);
641             }
642             return this;
643         }
644 
645         /**
646          * Gets the set of writable property names in these permissions.
647          *
648          * @return the set of property names
649          */
650         public Names write() {
651             return write;
652         }
653 
654         /**
655          * Adds a list of writable property names to these permissions.
656          *
657          * @param propertyNames the property names
658          * @return this instance of permissions
659          */
660         public Permissions write(final String... propertyNames) {
661             for (final String propertyName : propertyNames) {
662                 write.add(propertyName);
663             }
664             return this;
665         }
666     }
667 
668     /**
669      * @deprecated since 3.2, use {@link BlockSet}
670      */
671     @Deprecated
672     public static final class BlackSet extends BlockSet {
673         /** Default constructor */
674         public BlackSet() { } // Keep Javadoc happy
675     }
676 
677     /**
678      * @deprecated since 3.2, use {@link AllowSet}
679      */
680     @Deprecated
681     public static final class WhiteSet extends AllowSet {
682         /** Default constructor */
683         public WhiteSet() { } // Keep Javadoc happy
684     }
685 
686     /**
687      * Use block() instead.
688      *
689      * @param clazz the blocked class name
690      * @return the permissions instance
691      * @deprecated 3.3
692      */
693     @Deprecated
694     public Permissions black(final String clazz) {
695         return block(clazz);
696     }
697     /**
698      * Gets the read permission value for a given property of a class.
699      *
700      * @param clazz the class name
701      * @param name  the property name
702      * @return null if not allowed, the name of the property to use otherwise
703      * @deprecated 3.3
704      */
705     @Deprecated
706     public String read(final String clazz, final String name) {
707         return get(clazz).read().get(name);
708     }
709 
710     /**
711      * Use allow() instead.
712      *
713      * @param clazz the allowed class name
714      * @return the permissions instance
715      * @deprecated 3.3
716      */
717     @Deprecated
718     public Permissions white(final String clazz) {
719         return allow(clazz);
720     }
721 }