View Javadoc
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 }