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