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