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.Modifier;
24  import java.lang.reflect.Proxy;
25  import java.util.Collections;
26  import java.util.HashMap;
27  import java.util.HashSet;
28  import java.util.Map;
29  import java.util.Objects;
30  import java.util.Set;
31  import java.util.function.BiPredicate;
32  
33  import org.apache.commons.jexl3.annotations.NoJexl;
34  import org.apache.commons.jexl3.introspection.JexlPermissions;
35  
36  /**
37   * Checks whether an element (ctor, field, or method) is visible by JEXL introspection.
38   * <p>The default implementation does this by checking if an element has been annotated with NoJexl.</p>
39   *
40   * <p>The NoJexl annotation allows a fine grain permission on executable objects (methods, fields, constructors).
41   * </p>
42   * <ul>
43   * <li>NoJexl of a package implies all classes (including derived classes), and all interfaces
44   * of that package are invisible to JEXL.</li>
45   * <li>NoJexl on a class implies this class, and all its derived classes are invisible to JEXL.</li>
46   * <li>NoJexl on a (public) field makes it not visible as a property to JEXL.</li>
47   * <li>NoJexl on a constructor prevents that constructor to be used to instantiate through 'new'.</li>
48   * <li>NoJexl on a method prevents that method and any of its overrides to be visible to JEXL.</li>
49   * <li>NoJexl on an interface prevents all methods of that interface and their overrides to be visible to JEXL.</li>
50   * </ul>
51   * <p>It is possible to define permissions on external library classes used for which the source code
52   * cannot be altered using an instance of permissions using {@link JexlPermissions#parse(String...)}.</p>
53   */
54  public class Permissions implements JexlPermissions {
55      /**
56       * Represents the ability to create a copy of an object.
57       * Any class implementing this interface must provide a concrete
58       * implementation for the {@code copy()} method, which returns
59       * a new instance of the object that is a logical copy of the original.
60       *
61       * @param <T> The type of object that can be copied
62       */
63      interface Copyable<T> {
64          T copy() ;
65      }
66  
67      /**
68       * Creates a copy of a map containing copyable values.
69       * @param map The map to copy
70       * @return The copy of the map
71       * @param <T> The type of Copyable values
72       */
73      static <T extends Copyable<T>> Map<String, T> copyMap(final Map<String, T> map) {
74          final Map<String, T> copy = new HashMap<>(map.size());
75          for (final Map.Entry<String, T> entry : map.entrySet()) {
76              copy.put(entry.getKey(), entry.getValue().copy());
77          }
78          return copy;
79      }
80  
81      /**
82       * Equivalent of @NoJexl on a ctor, a method, or a field in a class.
83       * <p>Field or method that are named are denied access.</p>
84       */
85      static class NoJexlClass implements Copyable<NoJexlClass> {
86          // the NoJexl method names (including ctor, name of class)
87          final Set<String> methodNames;
88          // the NoJexl field names
89          final Set<String> fieldNames;
90  
91          NoJexlClass() {
92              this(new HashSet<>(), new HashSet<>());
93          }
94  
95          NoJexlClass(final Set<String> methods, final Set<String> fields) {
96              methodNames = methods;
97              fieldNames = fields;
98          }
99  
100         @Override public NoJexlClass copy() {
101             return new NoJexlClass(new HashSet<>(methodNames), new HashSet<>(fieldNames));
102         }
103 
104         boolean deny(final Constructor<?> method) {
105             return methodNames.contains(method.getDeclaringClass().getSimpleName());
106         }
107 
108         boolean deny(final Field field) {
109             return isEmpty() || fieldNames.contains(field.getName());
110         }
111 
112         boolean deny(final Method method) {
113             return isEmpty() || methodNames.contains(method.getName());
114         }
115 
116         boolean isEmpty() { return methodNames.isEmpty() && fieldNames.isEmpty(); }
117 
118         /**
119          * Whether this is a positive (allow-oriented) class declaration.
120          * <p>A positive class is explicitly allowed in its package (it contributes to
121          * {@link NoJexlPackage#hasAllowedClass()} and an empty one means &quot;allow the whole class&quot;).
122          * A plain deny-list {@code NoJexlClass} is not positive.</p>
123          *
124          * @return true if the class is positively allowed, false if it is a deny-list
125          */
126         boolean isPositive() { return false; }
127     }
128 
129     /**
130      * A positive NoJexl construct that defines what is denied by absence in the set.
131      * <p>Field or method that are named are the only one allowed access.</p>
132      */
133     static class JexlClass extends NoJexlClass {
134         JexlClass(final Set<String> methods, final Set<String> fields) {
135             super(methods, fields);
136         }
137         JexlClass() {
138         }
139         @Override public JexlClass copy() {
140             return new JexlClass(new HashSet<>(methodNames), new HashSet<>(fieldNames));
141         }
142         @Override boolean deny(final Constructor<?> method) { return !super.deny(method); }
143         @Override boolean deny(final Field field) { return !super.deny(field); }
144         @Override boolean deny(final Method method) { return !super.deny(method); }
145         @Override boolean isPositive() { return true; }
146     }
147 
148     /**
149      * A class that is positively allowed in its package yet carries deny-list member semantics.
150      * <p>Used for {@code +ClassName{ -member(); }} inside a deny package: the class is added to the
151      * allowed set (so unlisted classes in the package stay denied) but the named members are denied
152      * while all others remain allowed. It is a {@link NoJexlClass} (deny-list) tagged as positive;
153      * the deny-list behavior is inherited unchanged.</p>
154      */
155     static class AllowedNoJexlClass extends NoJexlClass {
156         AllowedNoJexlClass() {
157         }
158         AllowedNoJexlClass(final Set<String> methods, final Set<String> fields) {
159             super(methods, fields);
160         }
161         @Override public AllowedNoJexlClass copy() {
162             return new AllowedNoJexlClass(new HashSet<>(methodNames), new HashSet<>(fieldNames));
163         }
164         @Override boolean isPositive() { return true; }
165     }
166 
167     /**
168      * Equivalent of @NoJexl on a class in a package.
169      */
170     protected static class NoJexlPackage implements Copyable<NoJexlPackage> {
171         // the NoJexl class names
172         final Map<String, NoJexlClass> nojexl;
173 
174         /**
175          * Default ctor.
176          */
177         NoJexlPackage() {
178             this(null);
179         }
180 
181         /**
182          * Ctor.
183          *
184          * @param map The map of NoJexl classes
185          */
186         NoJexlPackage(final Map<String, NoJexlClass> map) {
187             this.nojexl = map == null || map.isEmpty() ? new HashMap<>() : map;
188         }
189 
190         void addNoJexl(final String key, final NoJexlClass njc) {
191             if (njc == null) {
192                 nojexl.remove(key);
193             } else {
194                 nojexl.put(key, njc);
195             }
196         }
197 
198         NoJexlClass getNoJexl(final Class<?> clazz) {
199             return nojexl.get(classKey(clazz));
200         }
201 
202         boolean isEmpty() { return nojexl.isEmpty(); }
203 
204         /**
205          * Whether this package has at least one explicitly-allowed class (a positive entry).
206          * <p>A package with allowed-class entries acts as an allow-list: unlisted classes are denied.
207          * A package with only denied-class entries acts as a deny-list: unlisted classes are allowed.</p>
208          *
209          * @return true if at least one class is explicitly allowed
210          */
211         boolean hasAllowedClass() {
212             for (final NoJexlClass njc : nojexl.values()) {
213                 if (njc.isPositive()) {
214                     return true;
215                 }
216             }
217             return false;
218         }
219 
220         /**
221          * Whether this is a positive (allow-oriented) package declaration.
222          * <p>A positive package allows its own classes by default; a deny-list package does not.</p>
223          *
224          * @return true if the package is positively allowed, false otherwise
225          */
226         boolean isPositive() { return false; }
227 
228         /**
229          * Whether this package denies all unlisted classes (deny-all-unlisted semantics).
230          * <p>Returns {@code true} for the {@link Markers#NOJEXL_PACKAGE} sentinel and
231          * {@link DenyAllPackage} instances; {@code false} for all other package types.</p>
232          *
233          * @return true if unlisted classes are denied by default
234          */
235         boolean isDenyAll() { return false; }
236 
237         @Override public NoJexlPackage copy() {
238             return new NoJexlPackage(copyMap(nojexl));
239         }
240     }
241 
242     /**
243      * A package where classes are allowed by default.
244      */
245     static class JexlPackage extends NoJexlPackage {
246         JexlPackage(final Map<String, NoJexlClass> map) {
247             super(map);
248         }
249 
250         @Override
251         NoJexlClass getNoJexl(final Class<?> clazz) {
252             final NoJexlClass njc = nojexl.get(classKey(clazz));
253             return njc != null ? njc : JEXL_CLASS;
254         }
255 
256         @Override boolean isPositive() { return true; }
257 
258         @Override public JexlPackage copy() {
259             return new JexlPackage(copyMap(nojexl));
260         }
261     }
262 
263     /**
264      * A package where ALL unlisted classes are denied.
265      * <p>The symmetric counterpart of {@link JexlPackage}: created by {@code compose()} when class-specific
266      * exceptions are added to a package previously marked as {@link Markers#NOJEXL_PACKAGE}.
267      * The deny-all-unlisted semantics of the base are preserved while allowing individually declared classes.</p>
268      */
269     static class DenyAllPackage extends NoJexlPackage {
270         DenyAllPackage(final Map<String, NoJexlClass> map) {
271             super(map);
272         }
273 
274         @Override
275         NoJexlClass getNoJexl(final Class<?> clazz) {
276             final NoJexlClass njc = nojexl.get(classKey(clazz));
277             return njc != null ? njc : NOJEXL_CLASS;
278         }
279 
280         @Override boolean isDenyAll() { return true; }
281 
282         @Override public NoJexlPackage copy() {
283             return new DenyAllPackage(copyMap(nojexl));
284         }
285     }
286 
287     /**
288      * Holder for the singleton allow/deny markers and the {@code UNRESTRICTED} permission.
289      * <p>These live in their own class rather than directly in {@link Permissions} to break a
290      * class-initialization cycle: {@code Permissions implements JexlPermissions}, and
291      * {@link JexlPermissions} carries default methods, so initializing {@code Permissions} forces
292      * {@code JexlPermissions.<clinit>} to run first (JLS 12.4.2) — that computes {@code RESTRICTED}
293      * / {@code SECURE} via {@link PermissionsParser}, which reads these markers. Were they static
294      * fields of {@code Permissions}, they would still be {@code null} at that point (assigned only
295      * after superinterface initialization), yielding an NPE when {@code Permissions} happens to be
296      * the first permissions class touched. A dedicated holder initializes independently of that
297      * cycle, so the markers are always available whichever class is loaded first.</p>
298      */
299     static final class Markers {
300         private Markers() {}
301 
302         /** Marker for whole NoJexl class. */
303         static final NoJexlClass NOJEXL_CLASS = new NoJexlClass(Collections.emptySet(), Collections.emptySet()) {
304             @Override boolean deny(final Constructor<?> method) {
305                 return true;
306             }
307 
308             @Override boolean deny(final Field field) {
309                 return true;
310             }
311 
312             @Override boolean deny(final Method method) {
313                 return true;
314             }
315 
316             // a constant singleton survives copy as itself, preserving its deny-all nature through compose()
317             @Override public NoJexlClass copy() {
318                 return this;
319             }
320         };
321 
322         /**
323          * Marker for a whole NoJexl class whose block only scopes nested-class declarations.
324          * <p>Behaves exactly like {@link #NOJEXL_CLASS} for the class itself but is distinct, so that
325          * the nested-class denial inheritance applied to {@code -X{}} does not deny the unlisted
326          * nested classes of a block that is a mere container (as in {@code Outer { Inner {...} }}).</p>
327          */
328         static final NoJexlClass NOJEXL_CONTAINER = new NoJexlClass(Collections.emptySet(), Collections.emptySet()) {
329             @Override boolean deny(final Constructor<?> method) {
330                 return true;
331             }
332 
333             @Override boolean deny(final Field field) {
334                 return true;
335             }
336 
337             @Override boolean deny(final Method method) {
338                 return true;
339             }
340 
341             // a constant singleton survives copy as itself, preserving its semantics through compose()
342             @Override public NoJexlClass copy() {
343                 return this;
344             }
345         };
346 
347         /** Marker for allowed class. */
348         static final NoJexlClass JEXL_CLASS = new JexlClass(Collections.emptySet(), Collections.emptySet()) {
349             @Override boolean deny(final Constructor<?> method) {
350                 return false;
351             }
352 
353             @Override boolean deny(final Field field) {
354                 return false;
355             }
356 
357             @Override  boolean deny(final Method method) {
358                 return false;
359             }
360 
361             // a constant singleton survives copy as itself, preserving its allow-all nature through compose()
362             @Override public JexlClass copy() {
363                 return this;
364             }
365         };
366 
367         /** Marker for @NoJexl package. */
368         static final NoJexlPackage NOJEXL_PACKAGE = new NoJexlPackage(Collections.emptyMap()) {
369             @Override NoJexlClass getNoJexl(final Class<?> clazz) {
370                 return NOJEXL_CLASS;
371             }
372 
373             @Override boolean isDenyAll() { return true; }
374 
375             // a constant singleton survives copy as itself, preserving its deny-all nature through compose()
376             @Override public NoJexlPackage copy() {
377                 return this;
378             }
379         };
380 
381         /** Marker for fully allowed package. */
382         static final NoJexlPackage JEXL_PACKAGE = new NoJexlPackage(Collections.emptyMap()) {
383             @Override NoJexlClass getNoJexl(final Class<?> clazz) {
384                 return JEXL_CLASS;
385             }
386             @Override boolean isPositive() {
387                 return true;
388             }
389             // a constant singleton survives copy as itself, preserving its positive nature through compose()
390             @Override public NoJexlPackage copy() {
391                 return this;
392             }
393         };
394 
395         /**
396          * The no-restriction permission singleton (empty {@link PermissionsParser#parse}).
397          * <p>Lives here rather than as a {@code Permissions} static field for the same reason as the
398          * markers: an empty {@code parse()} returns this instance while computing {@code JexlPermissions}
399          * constants, which can happen before {@code Permissions}' own static fields are assigned.
400          * Constructing it last (after the markers) is safe: the {@code new Permissions()} triggers
401          * {@code Permissions.<clinit>}, whose only holder dependency is the already-assigned markers.</p>
402          */
403         static final Permissions UNRESTRICTED = new Permissions();
404     }
405 
406     /** Marker for whole NoJexl class. */
407     static final NoJexlClass NOJEXL_CLASS = Markers.NOJEXL_CLASS;
408 
409     /** Marker for a NoJexl class that only scopes nested-class declarations. */
410     static final NoJexlClass NOJEXL_CONTAINER = Markers.NOJEXL_CONTAINER;
411 
412     /** Marker for allowed class. */
413     static final NoJexlClass JEXL_CLASS = Markers.JEXL_CLASS;
414 
415     /** Marker for @NoJexl package. */
416     static final NoJexlPackage NOJEXL_PACKAGE = Markers.NOJEXL_PACKAGE;
417 
418     /** Marker for fully allowed package. */
419     static final NoJexlPackage JEXL_PACKAGE = Markers.JEXL_PACKAGE;
420 
421     /**
422      * The @NoJexl execution-time map.
423      */
424     private final Map<String, NoJexlPackage> packages;
425     /**
426      * The allowed package patterns (wildcards or exact package names).
427      * <p>Empty together with an empty {@link #packages} map means open-world: every package is accessible
428      * and only explicitly denied elements are carved out — the behavior of {@link Markers#UNRESTRICTED}.
429      * Empty with a non-empty {@link #packages} map, or non-empty, means closed-world: only declared
430      * packages are accessible.</p>
431      */
432     private final Set<String> allowed;
433 
434     /** Allow inheritance. */
435     protected Permissions() {
436         this.allowed = Collections.emptySet();
437         this.packages = Collections.emptyMap();
438     }
439 
440     /**
441      * Default ctor.
442      *
443      * @param perimeter The allowed wildcard set of packages
444      * @param nojexl The NoJexl external map
445      */
446     protected Permissions(final Set<String> perimeter, final Map<String, NoJexlPackage> nojexl) {
447         this.allowed = perimeter;
448         this.packages = nojexl;
449     }
450 
451     /**
452      * Creates a new set of permissions by composing these permissions with a new set of rules.
453      *
454      * @param src The rules
455      * @return The new permissions
456      */
457     @Override
458     public Permissions compose(final String... src) {
459         return new PermissionsParser().parse(new HashSet<>(allowed), copyMap(packages), src);
460     }
461 
462     /**
463      * Creates a class key joining enclosing ascendants with '$'.
464      * <p>As in {@code outer$inner} for <code>class outer { class inner...</code>.</p>
465      *
466      * @param clazz The clazz
467      * @return The clazz key
468      */
469     static String classKey(final Class<?> clazz) {
470         return classKey(clazz, null);
471     }
472 
473     /**
474      * Creates a class key joining enclosing ascendants with '$'.
475      * <p>As in {@code outer$inner} for <code>class outer { class inner...</code>.</p>
476      *
477      * @param clazz The clazz
478      * @param strb The buffer to compose the key
479      * @return The clazz key
480      */
481     static String classKey(final Class<?> clazz, final StringBuilder strb) {
482         StringBuilder keyb = strb;
483         final Class<?> outer = clazz.getEnclosingClass();
484         if (outer != null) {
485             if (keyb == null) {
486                 keyb = new StringBuilder();
487             }
488             classKey(outer, keyb);
489             keyb.append('$');
490         }
491         if (keyb != null) {
492             keyb.append(clazz.getSimpleName());
493             return keyb.toString();
494         }
495         return clazz.getSimpleName();
496     }
497 
498     /**
499      * Whether the wildcard set of packages allows a given package to be introspected.
500      *
501      * @param allowed The allowed set (not null, may be empty)
502      * @param name The package name (not null)
503      * @return true if allowed, false otherwise
504      */
505     static boolean wildcardAllow(final Set<String> allowed, final String name) {
506         // allowed packages are explicit in this case
507         boolean found = allowed == null || allowed.isEmpty() || allowed.contains(name);
508         if (!found) {
509             String wildcard = name;
510             for (int i = name.length(); !found && i > 0; i = wildcard.lastIndexOf('.')) {
511                 wildcard = wildcard.substring(0, i);
512                 found = allowed.contains(wildcard + ".*");
513             }
514         }
515         return found;
516     }
517 
518     /**
519      * Gets the package constraints.
520      *
521      * @param packageName The package name
522      * @return The package constraints instance, not-null.
523      */
524     private NoJexlPackage getNoJexlPackage(final String packageName) {
525         return packages.getOrDefault(packageName, JEXL_PACKAGE);
526     }
527 
528     /**
529      * @return The packages
530      */
531     Map<String, NoJexlPackage> getPackages() {
532         return packages == null ? Collections.emptyMap() : Collections.unmodifiableMap(packages);
533     }
534 
535     /**
536      * @return The wildcards
537      */
538     Set<String> getWildcards() {
539         return allowed == null ? Collections.emptySet() : Collections.unmodifiableSet(allowed);
540     }
541 
542     /**
543      * Whether a package belongs to the allowed perimeter.
544      * <p>Open-world ({@link Markers#UNRESTRICTED}: no rules at all) allows every package. Closed-world requires the
545      * package to match an entry in {@link #allowed}; an empty perimeter in closed-world matches nothing.</p>
546      *
547      * @param packageName The package name (not null)
548      * @return true if allowed, false otherwise
549      */
550     private boolean allowedPackage(final String packageName) {
551         if ((allowed.isEmpty() && packages.isEmpty()) || (!allowed.isEmpty() && wildcardAllow(allowed, packageName))) {
552             return true;
553         }
554         // a package explicitly declared positive allows itself (exact match only, no sub-package inference)
555         final NoJexlPackage njp = packages.get(packageName);
556         return njp != null && njp.isPositive();
557     }
558 
559     /**
560      * Determines whether a specified permission check is allowed for a given class.
561      * The check involves verifying if a class or its corresponding package explicitly permits
562      * a name (e.g., method) based on a given condition.
563      *
564      * @param <T> The type of the name to check (e.g., method, constructor)
565      * @param clazz The class to evaluate (not null)
566      * @param name The name to verify (not null)
567      * @param check The condition to test whether the specified name is allowed (not null)
568      * @return true if the specified name is allowed based on the condition, false otherwise
569      */
570     private <T> boolean specifiedAllow(final Class<?> clazz, final T name, final BiPredicate<NoJexlClass, T> check) {
571         final String packageName = ClassTool.getPackageName(clazz);
572         if (allowedPackage(packageName)) {
573             return true;
574         }
575         final NoJexlPackage njp = packages.get(packageName);
576         if (njp != null && check != null) {
577             // there is a package permission, check if there is a class permission
578             final NoJexlClass njc = njp.getNoJexl(clazz);
579             if (njc != null) {
580                 return check.test(njc, name);
581             }
582             // class not listed: allowed if the package is a deny-list (no explicit class allows);
583             // denied if the package is an allow-list (e.g. java.io -{ +PrintWriter{} ... })
584             return !njp.hasAllowedClass();
585         }
586         // package not declared at all
587         return false;
588     }
589 
590     /**
591      * Checks whether a class or one of its super-classes or implemented interfaces
592      * explicitly allows JEXL introspection.
593      *
594      * @param clazz The class to check
595      * @return true if JEXL is allowed to introspect, false otherwise
596      */
597     @Override
598     public boolean allow(final Class<?> clazz) {
599         // clazz must be not null
600         if (!validate(clazz)) {
601             return false;
602         }
603         // proxy goes through
604         if (Proxy.isProxyClass(clazz)) {
605             return true;
606         }
607         // class must not be denied, nor extend a denied class
608         if (deny(clazz)) {
609             return false;
610         }
611         // a subclass of a denied class is also denied; Object (the universal root) is skipped, and the
612         // null guard keeps the walk safe when clazz is an interface or Object itself (getSuperclass() null).
613         for (Class<?> walk = clazz.getSuperclass(); walk != null && walk != Object.class; walk = walk.getSuperclass()) {
614             if (deny(walk)) {
615                 return false;
616             }
617         }
618         // a class is allowed only by its own package or class-level declaration: there is deliberately no
619         // reach-through via an allowed super-type, which would otherwise expose the whole foreign class.
620         return allowedClass(clazz);
621     }
622 
623     /**
624      * Whether a class is allowed by its own package or class-level declaration.
625      *
626      * @param clazz The class to check (not null)
627      * @return true if explicitly allowed
628      */
629     private boolean allowedClass(final Class<?> clazz) {
630         return specifiedAllow(clazz, clazz, (njc, c) -> njc.isPositive() || !njc.isEmpty());
631     }
632 
633     /**
634      * Check whether a method is allowed to be introspected in one superclass or interface.
635      *
636      * @param clazz The superclass or interface to check
637      * @param method The method
638      * @param explicit carries whether the package holding the method is explicitly allowed
639      * @return true if JEXL is allowed to introspect, false otherwise
640      */
641     private boolean allow(final Class<?> clazz, final Method method, final boolean[] explicit) {
642         try {
643             // check if the method in that class is declared thus overrides
644             final Method override = clazz.getDeclaredMethod(method.getName(), method.getParameterTypes());
645             if (override != method) {
646                 // should not be possible...
647                 if (denyMethod(override)) {
648                     return false;
649                 }
650                 // explicit |= ...
651                 if (!explicit[0]) {
652                     explicit[0] = specifiedAllow(clazz, override, (njc, m) -> !njc.deny(m));
653                 }
654             }
655         } catch (final NoSuchMethodException ex) {
656             // will happen if not overriding method in clazz; still need to check parent interfaces
657         } catch (final SecurityException ex) {
658             // unexpected, can't do much
659             return false;
660         }
661         // recursively check parent interfaces
662         for (final Class<?> inter : clazz.getInterfaces()) {
663             if (!allow(inter, method, explicit)) {
664                 return false;
665             }
666         }
667         return true;
668     }
669 
670     /**
671      * Checks whether a constructor explicitly allows JEXL introspection.
672      *
673      * @param ctor The constructor to check
674      * @return true if JEXL is allowed to introspect, false otherwise
675      */
676     @Override
677     public boolean allow(final Constructor<?> ctor) {
678         // method must be not null, public
679         // check declared restrictions
680         if (!validate(ctor) || deny(ctor)) {
681             return false;
682         }
683         // class must agree
684         final Class<?> clazz = ctor.getDeclaringClass();
685         if (deny(clazz)) {
686             return false;
687         }
688         // check wildcards
689         return specifiedAllow(clazz, clazz, (njc, c) -> !njc.deny(ctor));
690     }
691 
692 
693     /**
694      * Checks whether a field explicitly allows JEXL introspection.
695      *
696      * @param field The field to check
697      * @return true if JEXL is allowed to introspect, false otherwise
698      */
699     @Override
700     public boolean allow(final Field field) {
701         // field must be public
702         // check declared restrictions
703         if (!validate(field) || deny(field)) {
704             return false;
705         }
706         // class must agree
707         final Class<?> clazz = field.getDeclaringClass();
708         if (deny(clazz)) {
709             return false;
710         }
711         // check wildcards
712         return specifiedAllow(clazz, field, (njc, m) -> !njc.deny(m));
713     }
714 
715     @Override
716     public boolean allow(final Class<?> clazz, final Method method) {
717         if (!validate(clazz) || !validate(method)) {
718             return false;
719         }
720         if ((method.getModifiers() & Modifier.STATIC) == 0) {
721             final Class<?> declaring = method.getDeclaringClass();
722             if (clazz != declaring) {
723                 // just check this is an override of a method in clazz, if not, it is not allowed (obviously)
724                 if (deny(clazz) || !declaring.isAssignableFrom(clazz)) {
725                     return false;
726                 }
727                 // an explicit permission on the concrete class can deny; otherwise fall through to
728                 // allow(method) which honors carve-outs on the declaring class/interfaces (e.g. Object.getClass())
729                 final NoJexlClass njc = getNoJexl(clazz, null);
730                 if (njc != null && njc.deny(method)) {
731                     return false;
732                 }
733             }
734         }
735         return allow(method);
736     }
737 
738     @Override
739     public boolean allow(final Class<?> clazz, final Field field) {
740         if (!validate(clazz) || !validate(field)) {
741             return false;
742         }
743         if ((field.getModifiers() & Modifier.STATIC) == 0) {
744             final Class<?> declaring = field.getDeclaringClass();
745             if (clazz != declaring) {
746                 // just check this clazz extends/inherits from declaring, if not, it is not allowed (obviously)
747                 if (deny(clazz) || !declaring.isAssignableFrom(clazz)) {
748                     return false;
749                 }
750                 // an explicit permission on the concrete class can deny; otherwise fall through to
751                 // allow(field) which honors carve-outs on the declaring class
752                 final NoJexlClass njc = getNoJexl(clazz, null);
753                 if (njc != null && njc.deny(field)) {
754                     return false;
755                 }
756             }
757         }
758         return allow(field);
759     }
760 
761     /**
762      * Checks whether a method explicitly allows JEXL introspection.
763      * <p>Since methods can be overridden, this also checks that no superclass or interface
764      * explicitly disallows this method.</p>
765      *
766      * @param method The method to check
767      * @return true if JEXL is allowed to introspect, false otherwise
768      */
769     @Override
770     public boolean allow(final Method method) {
771         // method must be not null, public, not synthetic, not bridge
772         // method must be allowed
773         if (!validate(method) || denyMethod(method)) {
774             return false;
775         }
776         Class<?> clazz = method.getDeclaringClass();
777         // gather if the packages explicitly allow any implementation of the method
778         final boolean[] explicit = { specifiedAllow(clazz, method, (njc, m) -> !njc.deny(m)) };
779         // let's walk all interfaces
780         for (final Class<?> inter : clazz.getInterfaces()) {
781             if (!allow(inter, method, explicit)) {
782                 return false;
783             }
784         }
785         // let's walk all super classes
786         clazz = clazz.getSuperclass();
787         while (clazz != null) {
788             if (!allow(clazz, method, explicit)) {
789                 return false;
790             }
791             clazz = clazz.getSuperclass();
792         }
793         return explicit[0];
794     }
795 
796     /**
797      * Checks whether a package explicitly disallows JEXL introspection.
798      *
799      * @param pack The package
800      * @return true if JEXL is allowed to introspect, false otherwise
801      */
802     @Override
803     public boolean allow(final Package pack) {
804         // field must be public
805         // check declared restrictions
806         if (!validate(pack) || deny(pack)) {
807             return false;
808         }
809         // an explicit package entry is allowed unless it is the deny marker
810         final String name = pack.getName();
811         final NoJexlPackage njp = packages.get(name);
812         return njp == null ? allowedPackage(name) : !Objects.equals(NOJEXL_PACKAGE, njp);
813     }
814 
815 
816     /**
817      * Tests whether a whole class is denied Jexl visibility.
818      * <p>Also checks package visibility.</p>
819      *
820      * @param clazz The class
821      * @return true if denied, false otherwise
822      */
823     private boolean deny(final Class<?> clazz) {
824         // Don't deny arrays
825         if (clazz.isArray()) {
826             return false;
827         }
828         // is clazz annotated with nojexl ?
829         final NoJexl nojexl = clazz.getAnnotation(NoJexl.class);
830         if (nojexl != null) {
831             return true;
832         }
833         final NoJexlPackage njp = packages.get(ClassTool.getPackageName(clazz));
834         if (njp == null) {
835             return false;
836         }
837         final NoJexlClass njc = njp.getNoJexl(clazz);
838         if (Objects.equals(NOJEXL_CLASS, njc) || Objects.equals(NOJEXL_CONTAINER, njc)) {
839             return true;
840         }
841         // a nested class without an explicit declaration of its own inherits a whole-class
842         // denial from any of its enclosing classes: -Outer{} also denies Outer$Nested
843         // (container blocks - Outer { Inner {...} } - deliberately do not propagate)
844         if (njp.nojexl.get(classKey(clazz)) == null) {
845             for (Class<?> outer = clazz.getEnclosingClass(); outer != null; outer = outer.getEnclosingClass()) {
846                 if (Objects.equals(NOJEXL_CLASS, njp.getNoJexl(outer))) {
847                     return true;
848                 }
849             }
850         }
851         return false;
852     }
853 
854     /**
855      * Tests whether a constructor is denied Jexl visibility.
856      *
857      * @param ctor The constructor
858      * @return true if denied, false otherwise
859      */
860     private boolean deny(final Constructor<?> ctor) {
861         // is ctor annotated with nojexl ?
862         final NoJexl nojexl = ctor.getAnnotation(NoJexl.class);
863         if (nojexl != null) {
864             return true;
865         }
866         return getNoJexl(ctor.getDeclaringClass()).deny(ctor);
867     }
868 
869     /**
870      * Tests whether a field is denied Jexl visibility.
871      *
872      * @param field The field
873      * @return true if denied, false otherwise
874      */
875     private boolean deny(final Field field) {
876         // is field annotated with nojexl ?
877         final NoJexl nojexl = field.getAnnotation(NoJexl.class);
878         if (nojexl != null) {
879             return true;
880         }
881         return getNoJexl(field.getDeclaringClass()).deny(field);
882     }
883 
884     /**
885      * Tests whether a method is denied Jexl visibility.
886      *
887      * @param method The method
888      * @return true if denied, false otherwise
889      */
890     private boolean deny(final Method method) {
891         // is method annotated with nojexl ?
892         final NoJexl nojexl = method.getAnnotation(NoJexl.class);
893         if (nojexl != null) {
894             return true;
895         }
896         return getNoJexl(method.getDeclaringClass()).deny(method);
897     }
898 
899     /**
900      * Tests whether a whole package is denied Jexl visibility.
901      *
902      * @param pack The package
903      * @return true if denied, false otherwise
904      */
905     private boolean deny(final Package pack) {
906         // is package annotated with nojexl ?
907         final NoJexl nojexl = pack.getAnnotation(NoJexl.class);
908         if (nojexl != null) {
909             return true;
910         }
911         return Objects.equals(NOJEXL_PACKAGE, packages.get(pack.getName()));
912     }
913 
914     /**
915      * Tests whether a method is denied.
916      *
917      * @param method The method
918      * @return true if it has been disallowed through annotation or declaration
919      */
920     private boolean denyMethod(final Method method) {
921         // check declared restrictions, class must not be denied
922         return deny(method) || deny(method.getDeclaringClass());
923     }
924 
925     /**
926      * Gets the class constraints.
927      * <p>If nothing was explicitly forbidden, everything is allowed.</p>
928      *
929      * @param clazz The class
930      * @return The class constraints instance, not-null.
931      */
932     private NoJexlClass getNoJexl(final Class<?> clazz) {
933         return getNoJexl(clazz, JEXL_CLASS);
934     }
935     private NoJexlClass getNoJexl(final Class<?> clazz, final NoJexlClass ifNone) {
936         final String pkgName = ClassTool.getPackageName(clazz);
937         final NoJexlPackage njp = getNoJexlPackage(pkgName);
938         if (njp != null) {
939             final NoJexlClass njc = njp.getNoJexl(clazz);
940             if (njc != null) {
941                 return njc;
942             }
943         }
944         return ifNone;
945     }
946 }