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