001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.rest.server.guard;
018
019import static org.apache.juneau.commons.lang.StateEnum.*;
020import static org.apache.juneau.commons.utils.CollectionUtils.*;
021import static org.apache.juneau.commons.utils.Shorts.*;
022import static org.apache.juneau.commons.utils.StringUtils.*;
023
024import java.text.*;
025import java.util.*;
026import java.util.regex.*;
027
028import org.apache.juneau.commons.lang.*;
029import org.apache.juneau.commons.utils.*;
030
031/**
032 * Utility class for matching JEE user roles against string expressions.
033 *
034 * <p>
035 * Supports the following expression constructs:
036 * <ul>
037 *    <li><js>"foo"</js> - Single arguments.
038 *    <li><js>"foo,bar,baz"</js> - Multiple OR'ed arguments.
039 *    <li><js>"foo | bar | bqz"</js> - Multiple OR'ed arguments, pipe syntax.
040 *    <li><js>"foo || bar || bqz"</js> - Multiple OR'ed arguments, Java-OR syntax.
041 *    <li><js>"fo*"</js> - Patterns including <js>'*'</js> and <js>'?'</js>.
042 *    <li><js>"fo* &amp; *oo"</js> - Multiple AND'ed arguments, ampersand syntax.
043 *    <li><js>"fo* &amp;&amp; *oo"</js> - Multiple AND'ed arguments, Java-AND syntax.
044 *    <li><js>"fo* || (*oo || bar)"</js> - Parenthesis.
045 * </ul>
046 *
047 * <h5 class='section'>Notes:</h5><ul>
048 *    <li class='note'>AND operations take precedence over OR operations (as expected).
049 *    <li class='note'>Whitespace is ignored.
050 *    <li class='note'><jk>null</jk> or empty expressions always match as <jk>false</jk>.
051 * </ul>
052 *
053 * <h5 class='section'>See Also:</h5><ul>
054 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Guards">Guards</a>
055 * </ul>
056 */
057public class RoleMatcher {
058
059   static final class And extends Exp {
060      Exp[] clauses;
061
062      And(List<Exp> clauses) {
063         this.clauses = clauses.toArray(new Exp[clauses.size()]);
064      }
065
066      @Override /* Overridden from Object */
067      public String toString() {
068         return "(& " + join(clauses, " ") + ')';
069      }
070
071      @Override /* Overridden from Exp */
072      void appendTokens(Set<String> set) {
073         for (var clause : clauses)
074            clause.appendTokens(set);
075      }
076
077      @Override /* Overridden from Exp */
078      boolean matches(Set<String> roles) {
079         for (var e : clauses)
080            if (! e.matches(roles))
081               return false;
082         return true;
083      }
084   }
085
086   static final class Eq extends Exp {
087      final String operand;
088
089      Eq(String operand) {
090         this.operand = operand;
091      }
092
093      @Override /* Overridden from Object */
094      public String toString() {
095         return "[= " + operand + "]";
096      }
097
098      @Override /* Overridden from Exp */
099      void appendTokens(Set<String> set) {
100         set.add(operand);
101      }
102
103      @Override /* Overridden from Exp */
104      boolean matches(Set<String> roles) {
105         for (var role : roles)
106            if (eq(operand, role))
107               return true;
108         return false;
109      }
110   }
111
112   abstract static sealed class Exp {
113
114      @SuppressWarnings({
115         "unused" // set parameter unused in this default no-op implementation; subclasses use it
116      })
117      void appendTokens(Set<String> set) {}
118
119      abstract boolean matches(Set<String> roles);
120   }
121
122   static final class Match extends Exp {
123      final Pattern p;
124      final String operand;
125
126      Match(String operand) {
127         this.operand = operand;
128         p = getMatchPattern(operand);
129      }
130
131      @Override /* Overridden from Object */
132      public String toString() {
133         return "[* " + p.pattern().replaceAll("\\\\[QE]", "") + "]";
134      }
135
136      @Override /* Overridden from Exp */
137      void appendTokens(Set<String> set) {
138         set.add(operand);
139      }
140
141      @Override /* Overridden from Exp */
142      boolean matches(Set<String> roles) {
143         for (var role : roles)
144            if (p.matcher(role).matches())
145               return true;
146         return false;
147      }
148   }
149
150   static final class Never extends Exp {
151      @Override /* Overridden from Object */
152      public String toString() {
153         return "(NEVER)";
154      }
155
156      @Override
157      boolean matches(Set<String> roles) {
158         return false;
159      }
160   }
161
162   static final class Or extends Exp {
163      Exp[] clauses;
164
165      Or(List<Exp> clauses) {
166         this.clauses = clauses.toArray(new Exp[clauses.size()]);
167      }
168
169      @Override /* Overridden from Object */
170      public String toString() {
171         return "(| " + join(clauses, " ") + ')';
172      }
173
174      @Override /* Overridden from Exp */
175      void appendTokens(Set<String> set) {
176         for (var clause : clauses)
177            clause.appendTokens(set);
178      }
179
180      @Override
181      boolean matches(Set<String> roles) {
182         for (var e : clauses)
183            if (e.matches(roles))
184               return true;
185         return false;
186      }
187   }
188
189   // @formatter:off
190   private static final AsciiSet WS = AsciiSet.of(" \t");
191   private static final AsciiSet OP = AsciiSet.of(",|&");
192   private static final AsciiSet META = AsciiSet.of("*?");
193   // @formatter:on
194
195   private static Exp parseOperand(String operand) {
196      boolean hasMeta = false;
197      for (var i = 0; i < operand.length() && ! hasMeta; i++) {
198         var c = operand.charAt(i);
199         hasMeta |= META.contains(c);
200      }
201      return hasMeta ? new Match(operand) : new Eq(operand);
202   }
203
204   private final Exp exp;
205
206   /**
207    * Constructor.
208    *
209    * @param expression The string expression.
210    *    <br>Can be <jk>null</jk> or empty/blank (never matches).
211    * @throws ParseException If the expression is malformed.
212    */
213   public RoleMatcher(String expression) throws ParseException {
214      this.exp = parse(expression);
215   }
216
217   /**
218    * Returns all the tokens used in this expression.
219    *
220    * @return All the tokens used in this expression.
221    */
222   public Set<String> getRolesInExpression() {
223      var set = new TreeSet<String>();
224      exp.appendTokens(set);
225      return set;
226   }
227
228   /**
229    * Returns <jk>true</jk> if the specified string matches this expression.
230    *
231    * @param roles The user roles.
232    *    <br>Can be <jk>null</jk> (returns <jk>false</jk>).
233    * @return
234    *    <jk>true</jk> if the specified string matches this expression.
235    *    <br>Always <jk>false</jk> if the set is <jk>null</jk>.
236    */
237   public boolean matches(Set<String> roles) {
238      return nn(roles) && exp.matches(roles);
239   }
240
241   @Override /* Overridden from Object */
242   public String toString() {
243      return exp.toString();
244   }
245
246   @SuppressWarnings({
247      "java:S125", // Inline /*...*/ documents implicit else condition
248      "java:S135", // Multiple break statements necessary for state machine error handling
249      "java:S3776", // Cognitive complexity acceptable for parser state machine
250      "java:S6541" // Thread-safe singleton pattern acceptable
251   })
252   private Exp parse(String expression) throws ParseException {
253      if (StringUtils.isBlank(expression))
254         return new Never();
255
256      expression = expression.trim();
257
258      List<Exp> ors = list();
259      List<Exp> ands = list();
260
261      StateEnum state = S1;
262      int i = 0;
263      int mark = -1;
264      int pDepth = 0;
265      boolean error = false;
266
267      for (i = 0; i < expression.length(); i++) {
268         var c = expression.charAt(i);
269         if (state == S1) {
270            // S1 = Looking for start
271            if (! WS.contains(c)) {
272               if (c == '(') {
273                  state = S2;
274                  pDepth = 0;
275                  mark = i + 1;
276               } else if (OP.contains(c)) {
277                  error = true;
278                  break;
279               } else {
280                  state = S3;
281                  mark = i;
282               }
283            }
284         } else if (state == S2) {
285            // S2 = Found [(], looking for [)].
286            if (c == '(')
287               pDepth++;
288            if (c == ')') {
289               if (pDepth > 0)
290                  pDepth--;
291               else {
292                  ands.add(parse(expression.substring(mark, i)));
293                  mark = -1;
294                  state = S4;
295               }
296            }
297         } else if (state == S3) {
298            // S3 = Found [A], looking for end of A.
299            if (WS.contains(c) || OP.contains(c)) {
300               ands.add(parseOperand(expression.substring(mark, i)));
301               mark = -1;
302               if (WS.contains(c)) {
303                  state = S4;
304               } else {
305                  i--;
306                  state = S5;
307               }
308            }
309         } else if (state == S4) {
310            // S4 = Found [A ], looking for & or | or ,.
311            if (! WS.contains(c)) {
312               if (OP.contains(c)) {
313                  i--;
314                  state = S5;
315               } else {
316                  error = true;
317                  break;
318               }
319            }
320         } else if (state == S5) {  // NOSONAR - State check necessary for state machine
321            // S5 = Found & or | or ,
322            if (c == '&') {
323               state = S6;
324            } else /* (c == '|' || c == ',') */ {
325               if (ands.size() == 1) {
326                  ors.add(ands.get(0));
327               } else {
328                  ors.add(new And(ands));
329               }
330               ands.clear();
331               if (c == '|') {
332                  state = S7;
333               } else {
334                  state = S1;
335               }
336            }
337         } else if (state == S6) {
338            // S6 = Found &, looking for & or other
339            if (! WS.contains(c)) {
340               if (c != '&')
341                  i--;
342               state = S1;
343            }
344         } else /* (state == S7) */ {
345            // S7 = Found |, looking for | or other
346            if (! WS.contains(c)) {
347               if (c != '|')
348                  i--;
349               state = S1;
350            }
351         }
352      }
353
354      if (error)
355         throw new ParseException("Invalid character in expression '" + expression + "' at position " + i + ". state=" + state, i);
356
357      if (state == S1)
358         throw new ParseException("Could not find beginning of clause in '" + expression + "'", i);
359      if (state == S2)
360         throw new ParseException("Could not find matching parenthesis in expression '" + expression + "'", i);
361      if (state == S5 || state == S6 || state == S7)
362         throw new ParseException("Dangling clause in expression '" + expression + "'", i);
363
364      if (mark != -1)
365         ands.add(parseOperand(expression.substring(mark, expression.length())));
366      if (ands.size() == 1)
367         ors.add(ands.get(0));
368      else
369         ors.add(new And(ands));
370
371      if (ors.size() == 1)
372         return ors.get(0);
373      return new Or(ors);
374   }
375}