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.util.LinkedHashSet;
21 import java.util.Map;
22 import java.util.Set;
23 import java.util.concurrent.ConcurrentHashMap;
24
25 /**
26 * A crude parser to configure permissions akin to NoJexl annotations.
27 * The syntax recognizes 2 types of permissions:
28 * <ul>
29 * <li>restricting access to packages, classes (and inner classes), methods and fields</li>
30 * <li>allowing access to a wildcard restricted set of packages</li>
31 * </ul>
32 * <p>
33 * Example:
34 * </p>
35 * <pre>
36 * my.allowed.packages.*
37 * another.allowed.package.*
38 * # nojexl like restrictions
39 * my.package {
40 * class0 {...
41 * class1 {...}
42 * class2 {
43 * ...
44 * class3 {}
45 * }
46 * # and eol comment
47 * class0(); # constructors
48 * method(); # method is not allowed
49 * field; # field
50 * } # end class0
51 * +class1 {
52 * method(); // only allowed method of class1
53 * }
54 * } # end package my.package
55 * </pre>
56 */
57 public class PermissionsParser {
58
59 /** The source. */
60 private String src;
61
62 /** The source size. */
63 private int size;
64
65 /** The @NoJexl execution-time map. */
66 private Map<String, Permissions.NoJexlPackage> packages;
67
68 /** The set of wildcard imports. */
69 private Set<String> wildcards;
70
71 /**
72 * Basic ctor.
73 */
74 public PermissionsParser() {
75 // nothing besides default member initialization
76 }
77
78 /**
79 * Clears this parser internals.
80 */
81 private void clear() {
82 src = null; size = 0; packages = null; wildcards = null;
83 }
84
85 /**
86 * Parses permissions from a source.
87 *
88 * @param wildcards The set of allowed packages
89 * @param packages The map of restricted elements
90 * @param srcs The sources
91 * @return The permissions map
92 */
93 synchronized Permissions parse(final Set<String> wildcards, final Map<String, Permissions.NoJexlPackage> packages,
94 final String... srcs) {
95 try {
96 if (srcs == null || srcs.length == 0) {
97 return Permissions.Markers.UNRESTRICTED;
98 }
99 this.packages = packages;
100 this.wildcards = wildcards;
101 for (final String source : srcs) {
102 this.src = source;
103 this.size = source.length();
104 readPackages();
105 }
106 return new Permissions(wildcards, packages);
107 } finally {
108 clear();
109 }
110 }
111
112 /**
113 * Parses permissions from a source.
114 *
115 * @param srcs The sources
116 * @return The permissions map
117 */
118 public Permissions parse(final String... srcs) {
119 return parse(new LinkedHashSet<>(), new ConcurrentHashMap<>(), srcs);
120 }
121
122 /**
123 * Reads a class permission.
124 *
125 * @param njpackage The owning package
126 * @param nojexl whether the restriction is explicitly denying (true) or allowing (false) members
127 * @param outer The outer class (if any)
128 * @param inner The inner class name (if any)
129 * @param offset The initial parsing position in the source
130 * @return The new parsing position
131 */
132 private int readClass(final Permissions.NoJexlPackage njpackage, final boolean nojexl, final String outer, final String inner, final int offset) {
133 final StringBuilder temp = new StringBuilder();
134 Permissions.NoJexlClass njclass = null;
135 String njname = null;
136 String identifier = inner;
137 boolean deny = nojexl;
138 boolean classPositive = false; // the class's own polarity (set at creation, never mutated)
139 boolean memberNegative = false; // whether the pending member/inner class is prefixed with '-'
140 boolean memberPositive = false; // whether the pending member/inner class is prefixed with '+'
141 boolean hasNested = false; // whether this (otherwise empty) class only encloses nested classes
142 int i = offset;
143 int j = -1;
144 boolean isMethod = false;
145 while(i < size) {
146 final char c = src.charAt(i);
147 // if no parsing progress can be made, we are in error
148 if (j >= i) {
149 throw new IllegalStateException(unexpected(c, i));
150 }
151 j = i;
152 // get rid of space
153 if (Character.isWhitespace(c)) {
154 i = readSpaces(i + 1);
155 continue;
156 }
157 // eol comment
158 if (c == '#') {
159 i = readEol(i + 1);
160 continue;
161 }
162 // end of class ?
163 if (njclass != null && c == '}') {
164 i += 1;
165 break;
166 }
167 // read an identifier, the class name
168 if (identifier == null) {
169 // negative or positive set ? mixing signs on the same element is an error
170 if (c == '-') {
171 if (memberPositive) {
172 throw new IllegalStateException(unexpected(c, i));
173 }
174 // a '-' before a member denies it; tracked for a possible deny-list upgrade
175 memberNegative = true;
176 i += 1;
177 } else if (c == '+') {
178 if (memberNegative) {
179 throw new IllegalStateException(unexpected(c, i));
180 }
181 memberPositive = true;
182 // a '+' only selects an allowing class when it prefixes the class name; once the class
183 // exists it must not flip the class polarity (which would leak into inner classes)
184 if (njclass == null) {
185 deny = false;
186 }
187 i += 1;
188 }
189 final int next = readIdentifier(temp, i);
190 if (i != next) {
191 identifier = temp.toString();
192 temp.setLength(0);
193 i = next;
194 continue;
195 }
196 }
197 // parse a class:
198 if (njclass == null) {
199 // we must have read the class ('identifier {'...)
200 if (identifier == null || c != '{') {
201 throw new IllegalStateException(unexpected(c, i));
202 }
203 // if we have a class, it has a name
204 njclass = deny ? new Permissions.NoJexlClass() : new Permissions.JexlClass();
205 classPositive = !deny;
206 memberNegative = false; // a class-level sign does not carry to members
207 memberPositive = false;
208 njname = outer != null ? outer + "$" + identifier : identifier;
209 njpackage.addNoJexl(njname, njclass);
210 identifier = null;
211 } else if (identifier != null) {
212 // class member mode
213 if (c == '{') {
214 // inner class: its own polarity comes from its explicit sign ('-' deny, '+' allow),
215 // defaulting to the enclosing class's polarity; the sign never mutates the outer class
216 final boolean innerDeny = memberNegative || !memberPositive && !classPositive;
217 i = readClass(njpackage, innerDeny, njname, identifier, i - 1);
218 identifier = null;
219 memberNegative = false; // an inner-class sign does not change the outer class
220 memberPositive = false;
221 hasNested = true; // this class encloses at least one nested class declaration
222 continue;
223 }
224 if (c == ';') {
225 // a '-' before a member of a positive class turns it into an allowed deny-list class
226 if (memberNegative && classPositive && !(njclass instanceof Permissions.AllowedNoJexlClass)) {
227 final Permissions.AllowedNoJexlClass anc =
228 new Permissions.AllowedNoJexlClass(njclass.methodNames, njclass.fieldNames);
229 njpackage.addNoJexl(njname, anc);
230 njclass = anc;
231 }
232 // field or method?
233 if (isMethod) {
234 njclass.methodNames.add(identifier);
235 isMethod = false;
236 } else {
237 njclass.fieldNames.add(identifier);
238 }
239 identifier = null;
240 memberNegative = false;
241 memberPositive = false;
242 } else if (c == '(' && !isMethod) {
243 // method; only one opening parenthesis allowed
244 isMethod = true;
245 } else if (c != ')' || src.charAt(i - 1) != '(') {
246 // closing parenthesis following opening one was expected
247 throw new IllegalStateException(unexpected(c, i));
248 }
249 }
250 i += 1;
251 }
252 // empty class means allow or deny all
253 if (njname != null) {
254 if (njclass.isEmpty()) {
255 njpackage.addNoJexl(njname,
256 njclass.isPositive()
257 ? Permissions.Markers.JEXL_CLASS
258 : hasNested
259 ? Permissions.Markers.NOJEXL_CONTAINER
260 : Permissions.Markers.NOJEXL_CLASS);
261 } else {
262 njpackage.addNoJexl(njname, njclass);
263 }
264 }
265 return i;
266 }
267
268 /**
269 * Reads a comment till end-of-line.
270 *
271 * @param offset initial position
272 * @return position after comment
273 */
274 private int readEol(final int offset) {
275 int i = offset;
276 while (i < size) {
277 final char c = src.charAt(i);
278 if (c == '\n') {
279 break;
280 }
281 i += 1;
282 }
283 return i;
284 }
285
286 /**
287 * Reads an identifier (optionally dot-separated).
288 *
289 * @param id The builder to fill the identifier character with
290 * @param offset The initial reading position
291 * @return The position after the identifier
292 */
293 private int readIdentifier(final StringBuilder id, final int offset) {
294 return readIdentifier(id, offset, false, false);
295 }
296
297 /**
298 * Reads an identifier (optionally dot-separated).
299 *
300 * @param id The builder to fill the identifier character with
301 * @param offset The initial reading position
302 * @param dot whether dots (.) are allowed
303 * @param star whether stars (*) are allowed
304 * @return The position after the identifier
305 */
306 private int readIdentifier(final StringBuilder id, final int offset, final boolean dot, final boolean star) {
307 int begin = -1;
308 boolean starf = star;
309 int i = offset;
310 char c = 0;
311 while (i < size) {
312 c = src.charAt(i);
313 // accumulate identifier characters
314 if (Character.isJavaIdentifierStart(c) && begin < 0) {
315 begin = i;
316 id.append(c);
317 } else if (Character.isJavaIdentifierPart(c) && begin >= 0) {
318 id.append(c);
319 } else if (dot && c == '.') {
320 if (src.charAt(i - 1) == '.') {
321 throw new IllegalStateException(unexpected(c, i));
322 }
323 id.append('.');
324 begin = -1;
325 } else if (starf && c == '*') {
326 id.append('*');
327 starf = false; // only one star
328 } else {
329 break;
330 }
331 i += 1;
332 }
333 // cant end with a dot
334 if (dot && c == '.') {
335 throw new IllegalStateException(unexpected(c, i));
336 }
337 return i;
338 }
339
340 /**
341 * Creates or merges a package entry when a '{' block is opened.
342 *
343 * @param existing the current value in the packages map, or {@code null} if absent
344 * @param deny whether the new entry is deny-oriented
345 * @param denyAll whether unlisted classes should be denied (preserve deny-all-unlisted semantics)
346 * @return the package instance to store
347 */
348 private static Permissions.NoJexlPackage mergePackage(
349 final Permissions.NoJexlPackage existing, final boolean deny, final boolean denyAll) {
350 final Map<String, Permissions.NoJexlClass> pkgMap = existing == null ? null : existing.nojexl;
351 if (deny) {
352 return denyAll
353 ? new Permissions.DenyAllPackage(pkgMap)
354 : new Permissions.NoJexlPackage(pkgMap);
355 }
356 return new Permissions.JexlPackage(pkgMap);
357 }
358
359 /**
360 * Reads a package permission.
361 */
362 private void readPackages() {
363 final StringBuilder temp = new StringBuilder();
364 Permissions.NoJexlPackage njpackage = null;
365 int i = 0;
366 int j = -1;
367 String pname = null;
368 Boolean negative = null;
369 while (i < size) {
370 final char c = src.charAt(i);
371 // if no parsing progress can be made, we are in error
372 if (j >= i) {
373 throw new IllegalStateException(unexpected(c, i));
374 }
375 j = i;
376 // get rid of space
377 if (Character.isWhitespace(c)) {
378 i = readSpaces(i + 1);
379 continue;
380 }
381 // eol comment
382 if (c == '#') {
383 i = readEol(i + 1);
384 continue;
385 }
386 // read the package qualified name
387 if (pname == null) {
388 final int next = readIdentifier(temp, i, true, true);
389 if (i != next) {
390 pname = temp.toString();
391 temp.setLength(0);
392 i = next;
393 // consume it if it is a wildcard declaration
394 if (pname.endsWith(".*")) {
395 wildcards.add(pname);
396 pname = null;
397 }
398 continue;
399 }
400 }
401 // package mode
402 if (njpackage == null) {
403 // negative or positive package
404 if (negative == null) {
405 if (c == '-') {
406 negative = true;
407 i += 1;
408 continue;
409 }
410 if (c == '+') {
411 negative = false;
412 i += 1;
413 continue;
414 }
415 // default is deny specified classes
416 //negative = true;
417 }
418 if (c == '{') {
419 final Boolean sign = negative;
420 njpackage = packages.compute(pname, (n, p) -> {
421 final boolean deny = sign == null ? (p == null || !p.isPositive()) : sign;
422 final boolean denyAll = sign == null && p != null && p.isDenyAll();
423 return mergePackage(p, deny, denyAll);
424 });
425 i += 1;
426 }
427 } else if (c == '}') {
428 // empty means the whole package; a positive package is recognized by its JexlPackage/JEXL_PACKAGE
429 // type in the map (it allows itself and anchors reach-through), so nothing is added to wildcards.
430 if (njpackage.isEmpty()) {
431 packages.put(pname, negative == null || negative
432 ? Permissions.Markers.NOJEXL_PACKAGE
433 : Permissions.Markers.JEXL_PACKAGE);
434 } else {
435 packages.put(pname, njpackage);
436 }
437 njpackage = null; // can restart anew
438 pname = null;
439 negative = null;
440 i += 1;
441 } else {
442 i = readClass(njpackage, true,null, null, i);
443 }
444 }
445 }
446
447 /**
448 * Reads spaces.
449 *
450 * @param offset initial position
451 * @return position after spaces
452 */
453 private int readSpaces(final int offset) {
454 int i = offset;
455 while (i < size) {
456 final char c = src.charAt(i);
457 if (!Character.isWhitespace(c)) {
458 break;
459 }
460 i += 1;
461 }
462 return i;
463 }
464
465 /**
466 * Compose a parsing error message.
467 *
468 * @param c The offending character
469 * @param i The offset position
470 * @return The error message
471 */
472 private String unexpected(final char c, final int i) {
473 return "unexpected '" + c + "'@" + i;
474 }
475 }