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 "allow the whole class").
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 }