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