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.internal.introspection;
19  
20  import java.lang.reflect.Constructor;
21  import java.lang.reflect.Field;
22  import java.lang.reflect.Method;
23  import java.lang.reflect.Proxy;
24  import java.util.Collections;
25  import java.util.HashSet;
26  import java.util.LinkedHashSet;
27  import java.util.Map;
28  import java.util.Objects;
29  import java.util.Set;
30  import java.util.concurrent.ConcurrentHashMap;
31  
32  import org.apache.commons.jexl3.annotations.NoJexl;
33  import org.apache.commons.jexl3.introspection.JexlPermissions;
34  
35  /**
36   * Checks whether an element (ctor, field or method) is visible by JEXL introspection.
37   * <p>Default implementation does this by checking if element has been annotated with NoJexl.</p>
38   *
39   * <p>The NoJexl annotation allows a fine grain permissions on executable objects (methods, fields, constructors).
40   * </p>
41   * <ul>
42   * <li>NoJexl of a package implies all classes (including derived classes) and all interfaces
43   * of that package are invisible to JEXL.</li>
44   * <li>NoJexl on a class implies this class and all its derived classes are invisible to JEXL.</li>
45   * <li>NoJexl on a (public) field makes it not visible as a property to JEXL.</li>
46   * <li>NoJexl on a constructor prevents that constructor to be used to instantiate through 'new'.</li>
47   * <li>NoJexl on a method prevents that method and any of its overrides to be visible to JEXL.</li>
48   * <li>NoJexl on an interface prevents all methods of that interface and their overrides to be visible to JEXL.</li>
49   * </ul>
50   * <p> It is possible to further refine permissions on classes used through libraries where source code form can
51   * not be altered using an instance of permissions using {@link JexlPermissions#parse(String...)}.</p>
52   */
53  public class Permissions implements JexlPermissions {
54  
55      /**
56       * A positive NoJexl construct that defines what is denied by absence in the set.
57       * <p>Field or method that are named are the only one allowed access.</p>
58       */
59      static class JexlClass extends NoJexlClass {
60          @Override boolean deny(final Constructor<?> method) { return !super.deny(method); }
61          @Override boolean deny(final Field field) { return !super.deny(field); }
62          @Override boolean deny(final Method method) { return !super.deny(method); }
63      }
64  
65      /**
66       * Equivalent of @NoJexl on a ctor, a method or a field in a class.
67       * <p>Field or method that are named are denied access.</p>
68       */
69      static class NoJexlClass {
70          // the NoJexl method names (including ctor, name of class)
71          protected final Set<String> methodNames;
72          // the NoJexl field names
73          protected final Set<String> fieldNames;
74  
75          NoJexlClass() {
76              this(new HashSet<>(), new HashSet<>());
77          }
78  
79          NoJexlClass(final Set<String> methods, final Set<String> fields) {
80              methodNames = methods;
81              fieldNames = fields;
82          }
83  
84          boolean deny(final Constructor<?> method) {
85              return methodNames.contains(method.getDeclaringClass().getSimpleName());
86          }
87  
88          boolean deny(final Field field) {
89              return fieldNames.contains(field.getName());
90          }
91  
92          boolean deny(final Method method) {
93              return methodNames.contains(method.getName());
94          }
95  
96          boolean isEmpty() { return methodNames.isEmpty() && fieldNames.isEmpty(); }
97      }
98  
99      /**
100      * Equivalent of @NoJexl on a class in a package.
101      */
102     static class NoJexlPackage {
103         // the NoJexl class names
104         protected final Map<String, NoJexlClass> nojexl;
105 
106         /**
107          * Default ctor.
108          */
109         NoJexlPackage() {
110             this(null);
111         }
112 
113         /**
114          * Ctor.
115          *
116          * @param map the map of NoJexl classes
117          */
118         NoJexlPackage(final Map<String, NoJexlClass> map) {
119             this.nojexl = new ConcurrentHashMap<>(map == null ? Collections.emptyMap() : map);
120         }
121 
122         void addNoJexl(final String key, final NoJexlClass njc) {
123             if (njc == null) {
124                 nojexl.remove(key);
125             } else {
126                 nojexl.put(key, njc);
127             }
128         }
129 
130         NoJexlClass getNoJexl(final Class<?> clazz) {
131             return nojexl.get(classKey(clazz));
132         }
133 
134         boolean isEmpty() { return nojexl.isEmpty(); }
135     }
136 
137     /** Marker for whole NoJexl class. */
138     static final NoJexlClass NOJEXL_CLASS = new NoJexlClass(Collections.emptySet(), Collections.emptySet()) {
139         @Override boolean deny(final Constructor<?> method) {
140             return true;
141         }
142 
143         @Override boolean deny(final Field field) {
144             return true;
145         }
146 
147         @Override boolean deny(final Method method) {
148             return true;
149         }
150     };
151 
152     /** Marker for allowed class. */
153     static final NoJexlClass JEXL_CLASS = new NoJexlClass(Collections.emptySet(), Collections.emptySet()) {
154         @Override boolean deny(final Constructor<?> method) {
155             return false;
156         }
157 
158         @Override boolean deny(final Field field) {
159             return false;
160         }
161 
162         @Override  boolean deny(final Method method) {
163             return false;
164         }
165     };
166 
167     /** Marker for @NoJexl package. */
168     static final NoJexlPackage NOJEXL_PACKAGE = new NoJexlPackage(Collections.emptyMap()) {
169         @Override NoJexlClass getNoJexl(final Class<?> clazz) {
170             return NOJEXL_CLASS;
171         }
172     };
173 
174     /** Marker for fully allowed package. */
175     static final NoJexlPackage JEXL_PACKAGE = new NoJexlPackage(Collections.emptyMap()) {
176         @Override NoJexlClass getNoJexl(final Class<?> clazz) {
177             return JEXL_CLASS;
178         }
179     };
180 
181     /**
182      * The no-restriction introspection permission singleton.
183      */
184     static final Permissions UNRESTRICTED = new Permissions();
185 
186     /**
187      * Creates a class key joining enclosing ascendants with '$'.
188      * <p>As in {@code outer$inner} for <code>class outer { class inner...</code>.</p>
189      *
190      * @param clazz the clazz
191      * @return the clazz key
192      */
193     static String classKey(final Class<?> clazz) {
194         return classKey(clazz, null);
195     }
196 
197     /**
198      * Creates a class key joining enclosing ascendants with '$'.
199      * <p>As in {@code outer$inner} for <code>class outer { class inner...</code>.</p>
200      *
201      * @param clazz the clazz
202      * @param strb the buffer to compose the key
203      * @return the clazz key
204      */
205     static String classKey(final Class<?> clazz, final StringBuilder strb) {
206         StringBuilder keyb = strb;
207         final Class<?> outer = clazz.getEnclosingClass();
208         if (outer != null) {
209             if (keyb == null) {
210                 keyb = new StringBuilder();
211             }
212             classKey(outer, keyb);
213             keyb.append('$');
214         }
215         if (keyb != null) {
216             keyb.append(clazz.getSimpleName());
217             return keyb.toString();
218         }
219         return clazz.getSimpleName();
220     }
221 
222     /**
223      * Whether the wilcard set of packages allows a given package to be introspected.
224      *
225      * @param allowed the allowed set (not null, may be empty)
226      * @param name the package name (not null)
227      * @return true if allowed, false otherwise
228      */
229     static boolean wildcardAllow(final Set<String> allowed, final String name) {
230         // allowed packages are explicit in this case
231         boolean found = allowed == null || allowed.isEmpty() || allowed.contains(name);
232         if (!found) {
233             String wildcard = name;
234             for (int i = name.length(); !found && i > 0; i = wildcard.lastIndexOf('.')) {
235                 wildcard = wildcard.substring(0, i);
236                 found = allowed.contains(wildcard + ".*");
237             }
238         }
239         return found;
240     }
241 
242     /**
243      * The @NoJexl execution-time map.
244      */
245     private final Map<String, NoJexlPackage> packages;
246 
247     /**
248      * The closed world package patterns.
249      */
250     private final Set<String> allowed;
251 
252     /** Allow inheritance. */
253     protected Permissions() {
254         this(Collections.emptySet(), Collections.emptyMap());
255     }
256 
257     /**
258      * Default ctor.
259      *
260      * @param perimeter the allowed wildcard set of packages
261      * @param nojexl the NoJexl external map
262      */
263     protected Permissions(final Set<String> perimeter, final Map<String, NoJexlPackage> nojexl) {
264         this.allowed = perimeter;
265         this.packages = nojexl;
266     }
267 
268     /**
269      * Checks whether a class or one of its super-classes or implemented interfaces
270      * explicitly disallows JEXL introspection.
271      *
272      * @param clazz the class to check
273      * @return true if JEXL is allowed to introspect, false otherwise
274      */
275     @Override
276     public boolean allow(final Class<?> clazz) {
277         // clazz must be not null
278         if (!validate(clazz)) {
279             return false;
280         }
281         // proxy goes through
282         if (Proxy.isProxyClass(clazz)) {
283             return true;
284         }
285         // class must be allowed
286         if (deny(clazz)) {
287             return false;
288         }
289         // no super class can be denied and at least one must be allowed
290         boolean explicit = wildcardAllow(clazz);
291         Class<?> walk = clazz.getSuperclass();
292         while (walk != null) {
293             if (deny(walk)) {
294                 return false;
295             }
296             if (!explicit) {
297                 explicit = wildcardAllow(walk);
298             }
299             walk = walk.getSuperclass();
300         }
301         // check wildcards
302         return explicit;
303     }
304 
305     /**
306      * Check whether a method is allowed to be introspected in one superclass or interface.
307      *
308      * @param clazz the superclass or interface to check
309      * @param method the method
310      * @param explicit carries whether the package holding the method is explicitly allowed
311      * @return true if JEXL is allowed to introspect, false otherwise
312      */
313     private boolean allow(final Class<?> clazz, final Method method, final boolean[] explicit) {
314         try {
315             // check if method in that class is declared ie overrides
316             final Method override = clazz.getDeclaredMethod(method.getName(), method.getParameterTypes());
317             // should not be possible...
318             if (denyMethod(override)) {
319                 return false;
320             }
321             // explicit |= ...
322             if (!explicit[0]) {
323                 explicit[0] = wildcardAllow(clazz);
324             }
325             return true;
326         } catch (final NoSuchMethodException ex) {
327             // will happen if not overriding method in clazz
328             return true;
329         } catch (final SecurityException ex) {
330             // unexpected, can't do much
331             return false;
332         }
333     }
334 
335     /**
336      * Checks whether a constructor explicitly disallows JEXL introspection.
337      *
338      * @param ctor the constructor to check
339      * @return true if JEXL is allowed to introspect, false otherwise
340      */
341     @Override
342     public boolean allow(final Constructor<?> ctor) {
343         // method must be not null, public
344         if (!validate(ctor)) {
345             return false;
346         }
347         // check declared restrictions
348         if (deny(ctor)) {
349             return false;
350         }
351         // class must agree
352         final Class<?> clazz = ctor.getDeclaringClass();
353         if (deny(clazz)) {
354             return false;
355         }
356         // check wildcards
357         return wildcardAllow(clazz);
358     }
359 
360     /**
361      * Checks whether a field explicitly disallows JEXL introspection.
362      *
363      * @param field the field to check
364      * @return true if JEXL is allowed to introspect, false otherwise
365      */
366     @Override
367     public boolean allow(final Field field) {
368         // field must be public
369         if (!validate(field)) {
370             return false;
371         }
372         // check declared restrictions
373         if (deny(field)) {
374             return false;
375         }
376         // class must agree
377         final Class<?> clazz = field.getDeclaringClass();
378         if (deny(clazz)) {
379             return false;
380         }
381         // check wildcards
382         return wildcardAllow(clazz);
383     }
384 
385     /**
386      * Checks whether a method explicitly disallows JEXL introspection.
387      * <p>Since methods can be overridden, this also checks that no superclass or interface
388      * explicitly disallows this methods.</p>
389      *
390      * @param method the method to check
391      * @return true if JEXL is allowed to introspect, false otherwise
392      */
393     @Override
394     public boolean allow(final Method method) {
395         // method must be not null, public, not synthetic, not bridge
396         if (!validate(method)) {
397             return false;
398         }
399         // method must be allowed
400         if (denyMethod(method)) {
401             return false;
402         }
403         Class<?> clazz = method.getDeclaringClass();
404         // gather if any implementation of the method is explicitly allowed by the packages
405         final boolean[] explicit = { wildcardAllow(clazz) };
406         // let's walk all interfaces
407         for (final Class<?> inter : clazz.getInterfaces()) {
408             if (!allow(inter, method, explicit)) {
409                 return false;
410             }
411         }
412         // let's walk all super classes
413         clazz = clazz.getSuperclass();
414         while (clazz != null) {
415             if (!allow(clazz, method, explicit)) {
416                 return false;
417             }
418             clazz = clazz.getSuperclass();
419         }
420         return explicit[0];
421     }
422 
423     /**
424      * Checks whether a package explicitly disallows JEXL introspection.
425      *
426      * @param pack the package
427      * @return true if JEXL is allowed to introspect, false otherwise
428      */
429     @Override
430     public boolean allow(final Package pack) {
431        return validate(pack) && !deny(pack);
432     }
433 
434     /**
435      * Creates a new set of permissions by composing these permissions with a new set of rules.
436      *
437      * @param src the rules
438      * @return the new permissions
439      */
440     @Override
441     public Permissions compose(final String... src) {
442         return new PermissionsParser().parse(new LinkedHashSet<>(allowed),new ConcurrentHashMap<>(packages), src);
443     }
444 
445     /**
446      * Tests whether a whole class is denied Jexl visibility.
447      * <p>Also checks package visibility.</p>
448      *
449      * @param clazz the class
450      * @return true if denied, false otherwise
451      */
452     private boolean deny(final Class<?> clazz) {
453         // Don't deny arrays
454         if (clazz.isArray()) {
455             return false;
456         }
457         // is clazz annotated with nojexl ?
458         final NoJexl nojexl = clazz.getAnnotation(NoJexl.class);
459         if (nojexl != null) {
460             return true;
461         }
462         final NoJexlPackage njp = packages.get(ClassTool.getPackageName(clazz));
463         return njp != null && Objects.equals(NOJEXL_CLASS, njp.getNoJexl(clazz));
464     }
465 
466     /**
467      * Tests whether a constructor is denied Jexl visibility.
468      *
469      * @param ctor the constructor
470      * @return true if denied, false otherwise
471      */
472     private boolean deny(final Constructor<?> ctor) {
473         // is ctor annotated with nojexl ?
474         final NoJexl nojexl = ctor.getAnnotation(NoJexl.class);
475         if (nojexl != null) {
476             return true;
477         }
478         return getNoJexl(ctor.getDeclaringClass()).deny(ctor);
479     }
480 
481     /**
482      * Tests whether a field is denied Jexl visibility.
483      *
484      * @param field the field
485      * @return true if denied, false otherwise
486      */
487     private boolean deny(final Field field) {
488         // is field annotated with nojexl ?
489         final NoJexl nojexl = field.getAnnotation(NoJexl.class);
490         if (nojexl != null) {
491             return true;
492         }
493         return getNoJexl(field.getDeclaringClass()).deny(field);
494     }
495 
496     /**
497      * Tests whether a method is denied Jexl visibility.
498      *
499      * @param method the method
500      * @return true if denied, false otherwise
501      */
502     private boolean deny(final Method method) {
503         // is method annotated with nojexl ?
504         final NoJexl nojexl = method.getAnnotation(NoJexl.class);
505         if (nojexl != null) {
506             return true;
507         }
508         return getNoJexl(method.getDeclaringClass()).deny(method);
509     }
510 
511     /**
512      * Tests whether a whole package is denied Jexl visibility.
513      *
514      * @param pack the package
515      * @return true if denied, false otherwise
516      */
517     private boolean deny(final Package pack) {
518         // is package annotated with nojexl ?
519         final NoJexl nojexl = pack.getAnnotation(NoJexl.class);
520         if (nojexl != null) {
521             return true;
522         }
523         return Objects.equals(NOJEXL_PACKAGE, packages.get(pack.getName()));
524     }
525 
526     /**
527      * Tests whether a method is denied.
528      *
529      * @param method the method
530      * @return true if it has been disallowed through annotation or declaration
531      */
532     private boolean denyMethod(final Method method) {
533         // check declared restrictions, class must not be denied
534         return deny(method) || deny(method.getDeclaringClass());
535     }
536 
537     /**
538      * Gets the class constraints.
539      * <p>If nothing was explicitly forbidden, everything is allowed.</p>
540      *
541      * @param clazz the class
542      * @return the class constraints instance, not-null.
543      */
544     private NoJexlClass getNoJexl(final Class<?> clazz) {
545         final String pkgName = ClassTool.getPackageName(clazz);
546         final NoJexlPackage njp = getNoJexlPackage(pkgName);
547         if (njp != null) {
548             final NoJexlClass njc = njp.getNoJexl(clazz);
549             if (njc != null) {
550                 return njc;
551             }
552         }
553         return JEXL_CLASS;
554     }
555 
556     /**
557      * Gets the package constraints.
558      *
559      * @param packageName the package name
560      * @return the package constraints instance, not-null.
561      */
562     private NoJexlPackage getNoJexlPackage(final String packageName) {
563         return packages.getOrDefault(packageName, JEXL_PACKAGE);
564     }
565 
566     /**
567      * @return the packages
568      */
569     Map<String, NoJexlPackage> getPackages() {
570         return packages == null ? Collections.emptyMap() : Collections.unmodifiableMap(packages);
571     }
572 
573     /**
574      * @return the wilcards
575      */
576     Set<String> getWildcards() {
577         return allowed == null ? Collections.emptySet() : Collections.unmodifiableSet(allowed);
578     }
579 
580     /**
581      * Whether the wildcard set of packages allows a given class to be introspected.
582      *
583      * @param clazz the package name (not null)
584      * @return true if allowed, false otherwise
585      */
586     private boolean wildcardAllow(final Class<?> clazz) {
587         return wildcardAllow(allowed, ClassTool.getPackageName(clazz));
588     }
589 }