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.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 is prefixed with '-'
140 int i = offset;
141 int j = -1;
142 boolean isMethod = false;
143 while(i < size) {
144 final char c = src.charAt(i);
145 // if no parsing progress can be made, we are in error
146 if (j >= i) {
147 throw new IllegalStateException(unexpected(c, i));
148 }
149 j = i;
150 // get rid of space
151 if (Character.isWhitespace(c)) {
152 i = readSpaces(i + 1);
153 continue;
154 }
155 // eol comment
156 if (c == '#') {
157 i = readEol(i + 1);
158 continue;
159 }
160 // end of class ?
161 if (njclass != null && c == '}') {
162 i += 1;
163 break;
164 }
165 // read an identifier, the class name
166 if (identifier == null) {
167 // negative or positive set ?
168 if (c == '-') {
169 // a '-' before a member denies it; tracked for a possible deny-list upgrade
170 memberNegative = true;
171 i += 1;
172 } else if (c == '+') {
173 deny = false;
174 i += 1;
175 }
176 final int next = readIdentifier(temp, i);
177 if (i != next) {
178 identifier = temp.toString();
179 temp.setLength(0);
180 i = next;
181 continue;
182 }
183 }
184 // parse a class:
185 if (njclass == null) {
186 // we must have read the class ('identifier {'...)
187 if (identifier == null || c != '{') {
188 throw new IllegalStateException(unexpected(c, i));
189 }
190 // if we have a class, it has a name
191 njclass = deny ? new Permissions.NoJexlClass() : new Permissions.JexlClass();
192 classPositive = !deny;
193 memberNegative = false; // a class-level sign does not carry to members
194 njname = outer != null ? outer + "$" + identifier : identifier;
195 njpackage.addNoJexl(njname, njclass);
196 identifier = null;
197 } else if (identifier != null) {
198 // class member mode
199 if (c == '{') {
200 // inner class
201 i = readClass(njpackage, deny, njname, identifier, i - 1);
202 identifier = null;
203 memberNegative = false; // an inner-class sign does not change the outer class
204 continue;
205 }
206 if (c == ';') {
207 // a '-' before a member of a positive class turns it into an allowed deny-list class
208 if (memberNegative && classPositive && !(njclass instanceof Permissions.AllowedNoJexlClass)) {
209 final Permissions.AllowedNoJexlClass anc =
210 new Permissions.AllowedNoJexlClass(njclass.methodNames, njclass.fieldNames);
211 njpackage.addNoJexl(njname, anc);
212 njclass = anc;
213 }
214 // field or method?
215 if (isMethod) {
216 njclass.methodNames.add(identifier);
217 isMethod = false;
218 } else {
219 njclass.fieldNames.add(identifier);
220 }
221 identifier = null;
222 memberNegative = false;
223 } else if (c == '(' && !isMethod) {
224 // method; only one opening parenthesis allowed
225 isMethod = true;
226 } else if (c != ')' || src.charAt(i - 1) != '(') {
227 // closing parenthesis following opening one was expected
228 throw new IllegalStateException(unexpected(c, i));
229 }
230 }
231 i += 1;
232 }
233 // empty class means allow or deny all
234 if (njname != null) {
235 if (njclass.isEmpty()) {
236 njpackage.addNoJexl(njname,
237 njclass.isPositive()
238 ? Permissions.JEXL_CLASS
239 : Permissions.NOJEXL_CLASS);
240 } else {
241 njpackage.addNoJexl(njname, njclass);
242 }
243 }
244 return i;
245 }
246
247 /**
248 * Reads a comment till end-of-line.
249 *
250 * @param offset initial position
251 * @return position after comment
252 */
253 private int readEol(final int offset) {
254 int i = offset;
255 while (i < size) {
256 final char c = src.charAt(i);
257 if (c == '\n') {
258 break;
259 }
260 i += 1;
261 }
262 return i;
263 }
264
265 /**
266 * Reads an identifier (optionally dot-separated).
267 *
268 * @param id the builder to fill the identifier character with
269 * @param offset the initial reading position
270 * @return the position after the identifier
271 */
272 private int readIdentifier(final StringBuilder id, final int offset) {
273 return readIdentifier(id, offset, false, false);
274 }
275
276 /**
277 * Reads an identifier (optionally dot-separated).
278 *
279 * @param id the builder to fill the identifier character with
280 * @param offset the initial reading position
281 * @param dot whether dots (.) are allowed
282 * @param star whether stars (*) are allowed
283 * @return the position after the identifier
284 */
285 private int readIdentifier(final StringBuilder id, final int offset, final boolean dot, final boolean star) {
286 int begin = -1;
287 boolean starf = star;
288 int i = offset;
289 char c = 0;
290 while (i < size) {
291 c = src.charAt(i);
292 // accumulate identifier characters
293 if (Character.isJavaIdentifierStart(c) && begin < 0) {
294 begin = i;
295 id.append(c);
296 } else if (Character.isJavaIdentifierPart(c) && begin >= 0) {
297 id.append(c);
298 } else if (dot && c == '.') {
299 if (src.charAt(i - 1) == '.') {
300 throw new IllegalStateException(unexpected(c, i));
301 }
302 id.append('.');
303 begin = -1;
304 } else if (starf && c == '*') {
305 id.append('*');
306 starf = false; // only one star
307 } else {
308 break;
309 }
310 i += 1;
311 }
312 // cant end with a dot
313 if (dot && c == '.') {
314 throw new IllegalStateException(unexpected(c, i));
315 }
316 return i;
317 }
318
319 /**
320 * Reads a package permission.
321 */
322 private void readPackages() {
323 final StringBuilder temp = new StringBuilder();
324 Permissions.NoJexlPackage njpackage = null;
325 int i = 0;
326 int j = -1;
327 String pname = null;
328 Boolean negative = null;
329 while (i < size) {
330 final char c = src.charAt(i);
331 // if no parsing progress can be made, we are in error
332 if (j >= i) {
333 throw new IllegalStateException(unexpected(c, i));
334 }
335 j = i;
336 // get rid of space
337 if (Character.isWhitespace(c)) {
338 i = readSpaces(i + 1);
339 continue;
340 }
341 // eol comment
342 if (c == '#') {
343 i = readEol(i + 1);
344 continue;
345 }
346 // read the package qualified name
347 if (pname == null) {
348 final int next = readIdentifier(temp, i, true, true);
349 if (i != next) {
350 pname = temp.toString();
351 temp.setLength(0);
352 i = next;
353 // consume it if it is a wildcard declaration
354 if (pname.endsWith(".*")) {
355 wildcards.add(pname);
356 pname = null;
357 }
358 continue;
359 }
360 }
361 // package mode
362 if (njpackage == null) {
363 // negative or positive package
364 if (negative == null) {
365 if (c == '-') {
366 negative = true;
367 i += 1;
368 continue;
369 }
370 if (c == '+') {
371 negative = false;
372 i += 1;
373 continue;
374 }
375 // default is deny specified classes
376 //negative = true;
377 }
378 if (c == '{') {
379 final Boolean specified = negative;
380 njpackage = packages.compute(pname,
381 (n, p) -> {
382 // if we haven't specified allow/deny,
383 // keep the existing specification if any, otherwise default to deny
384 final boolean deny = specified == null ? !(p instanceof Permissions.JexlPackage) : specified;
385 final Map<String, Permissions.NoJexlClass> pkgMap = p == null ? null : p.nojexl;
386 return deny
387 ? new Permissions.NoJexlPackage(pkgMap)
388 : new Permissions.JexlPackage(pkgMap);
389 }
390 );
391 i += 1;
392 }
393 } else if (c == '}') {
394 // empty means the whole package; a positive package is recognized by its JexlPackage/JEXL_PACKAGE
395 // type in the map (it allows itself and anchors reach-through), so nothing is added to wildcards.
396 if (njpackage.isEmpty()) {
397 packages.put(pname, negative == null || negative
398 ? Permissions.NOJEXL_PACKAGE
399 : Permissions.JEXL_PACKAGE);
400 } else {
401 packages.put(pname, njpackage);
402 }
403 njpackage = null; // can restart anew
404 pname = null;
405 negative = null;
406 i += 1;
407 } else {
408 i = readClass(njpackage, true,null, null, i);
409 }
410 }
411 }
412
413 /**
414 * Reads spaces.
415 *
416 * @param offset initial position
417 * @return position after spaces
418 */
419 private int readSpaces(final int offset) {
420 int i = offset;
421 while (i < size) {
422 final char c = src.charAt(i);
423 if (!Character.isWhitespace(c)) {
424 break;
425 }
426 i += 1;
427 }
428 return offset;
429 }
430
431 /**
432 * Compose a parsing error message.
433 *
434 * @param c the offending character
435 * @param i the offset position
436 * @return the error message
437 */
438 private String unexpected(final char c, final int i) {
439 return "unexpected '" + c + "'@" + i;
440 }
441 }