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.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 }