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