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