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.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.StringUtils.*;
022import static org.apache.juneau.commons.utils.Utils.*;
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 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 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 (operand.equals(role))
107               return true;
108         return false;
109      }
110   }
111
112   abstract static class Exp {
113
114      void appendTokens(Set<String> set) {}
115
116      abstract boolean matches(Set<String> roles);
117   }
118
119   static class Match extends Exp {
120      final Pattern p;
121      final String operand;
122
123      Match(String operand) {
124         this.operand = operand;
125         p = getMatchPattern(operand);
126      }
127
128      @Override /* Overridden from Object */
129      public String toString() {
130         return "[* " + p.pattern().replaceAll("\\\\[QE]", "") + "]";
131      }
132
133      @Override /* Overridden from Exp */
134      void appendTokens(Set<String> set) {
135         set.add(operand);
136      }
137
138      @Override /* Overridden from Exp */
139      boolean matches(Set<String> roles) {
140         for (var role : roles)
141            if (p.matcher(role).matches())
142               return true;
143         return false;
144      }
145   }
146
147   static class Never extends Exp {
148      @Override /* Overridden from Object */
149      public String toString() {
150         return "(NEVER)";
151      }
152
153      @Override
154      boolean matches(Set<String> roles) {
155         return false;
156      }
157   }
158
159   static class Or extends Exp {
160      Exp[] clauses;
161
162      Or(List<Exp> clauses) {
163         this.clauses = clauses.toArray(new Exp[clauses.size()]);
164      }
165
166      @Override /* Overridden from Object */
167      public String toString() {
168         return "(| " + join(clauses, " ") + ')';
169      }
170
171      @Override /* Overridden from Exp */
172      void appendTokens(Set<String> set) {
173         for (var clause : clauses)
174            clause.appendTokens(set);
175      }
176
177      @Override
178      boolean matches(Set<String> roles) {
179         for (var e : clauses)
180            if (e.matches(roles))
181               return true;
182         return false;
183      }
184   }
185
186   // @formatter:off
187   private static final AsciiSet
188      WS = AsciiSet.of(" \t"),
189      OP = AsciiSet.of(",|&"),
190      META = AsciiSet.of("*?");
191   // @formatter:on
192
193   private static Exp parseOperand(String operand) {
194      boolean hasMeta = false;
195      for (var i = 0; i < operand.length() && ! hasMeta; i++) {
196         var c = operand.charAt(i);
197         hasMeta |= META.contains(c);
198      }
199      return hasMeta ? new Match(operand) : new Eq(operand);
200   }
201
202   private final Exp exp;
203
204   /**
205    * Constructor.
206    *
207    * @param expression The string expression.
208    * @throws ParseException If the expression is malformed.
209    */
210   public RoleMatcher(String expression) throws ParseException {
211      this.exp = parse(expression);
212   }
213
214   /**
215    * Returns all the tokens used in this expression.
216    *
217    * @return All the tokens used in this expression.
218    */
219   public Set<String> getRolesInExpression() {
220      var set = new TreeSet<String>();
221      exp.appendTokens(set);
222      return set;
223   }
224
225   /**
226    * Returns <jk>true</jk> if the specified string matches this expression.
227    *
228    * @param roles The user roles.
229    * @return
230    *    <jk>true</jk> if the specified string matches this expression.
231    *    <br>Always <jk>false</jk> if the string is <jk>null</jk>.
232    */
233   public boolean matches(Set<String> roles) {
234      return nn(roles) && exp.matches(roles);
235   }
236
237   @Override /* Overridden from Object */
238   public String toString() {
239      return exp.toString();
240   }
241
242   private Exp parse(String expression) throws ParseException {
243      if (StringUtils.isBlank(expression))
244         return new Never();
245
246      expression = expression.trim();
247
248      List<Exp> ors = list();
249      List<Exp> ands = list();
250
251      StateEnum state = S1;
252      int i = 0, mark = -1;
253      int pDepth = 0;
254      boolean error = false;
255
256      for (i = 0; i < expression.length(); i++) {
257         var c = expression.charAt(i);
258         if (state == S1) {
259            // S1 = Looking for start
260            if (! WS.contains(c)) {
261               if (c == '(') {
262                  state = S2;
263                  pDepth = 0;
264                  mark = i + 1;
265               } else if (OP.contains(c)) {
266                  error = true;
267                  break;
268               } else {
269                  state = S3;
270                  mark = i;
271               }
272            }
273         } else if (state == S2) {
274            // S2 = Found [(], looking for [)].
275            if (c == '(')
276               pDepth++;
277            if (c == ')') {
278               if (pDepth > 0)
279                  pDepth--;
280               else {
281                  ands.add(parse(expression.substring(mark, i)));
282                  mark = -1;
283                  state = S4;
284               }
285            }
286         } else if (state == S3) {
287            // S3 = Found [A], looking for end of A.
288            if (WS.contains(c) || OP.contains(c)) {
289               ands.add(parseOperand(expression.substring(mark, i)));
290               mark = -1;
291               if (WS.contains(c)) {
292                  state = S4;
293               } else {
294                  i--;
295                  state = S5;
296               }
297            }
298         } else if (state == S4) {
299            // S4 = Found [A ], looking for & or | or ,.
300            if (! WS.contains(c)) {
301               if (OP.contains(c)) {
302                  i--;
303                  state = S5;
304               } else {
305                  error = true;
306                  break;
307               }
308            }
309         } else if (state == S5) {
310            // S5 = Found & or | or ,.
311            if (c == '&') {
312               //ands.add(operand);
313               state = S6;
314            } else /* (c == '|' || c == ',') */ {
315               if (ands.size() == 1) {
316                  ors.add(ands.get(0));
317               } else {
318                  ors.add(new And(ands));
319               }
320               ands.clear();
321               if (c == '|') {
322                  state = S7;
323               } else {
324                  state = S1;
325               }
326            }
327         } else if (state == S6) {
328            // S6 = Found &, looking for & or other
329            if (! WS.contains(c)) {
330               if (c != '&')
331                  i--;
332               state = S1;
333            }
334         } else /* (state == S7) */ {
335            // S7 = Found |, looking for | or other
336            if (! WS.contains(c)) {
337               if (c != '|')
338                  i--;
339               state = S1;
340            }
341         }
342      }
343
344      if (error)
345         throw new ParseException("Invalid character in expression '" + expression + "' at position " + i + ". state=" + state, i);
346
347      if (state == S1)
348         throw new ParseException("Could not find beginning of clause in '" + expression + "'", i);
349      if (state == S2)
350         throw new ParseException("Could not find matching parenthesis in expression '" + expression + "'", i);
351      if (state == S5 || state == S6 || state == S7)
352         throw new ParseException("Dangling clause in expression '" + expression + "'", i);
353
354      if (mark != -1)
355         ands.add(parseOperand(expression.substring(mark, expression.length())));
356      if (ands.size() == 1)
357         ors.add(ands.get(0));
358      else
359         ors.add(new And(ands));
360
361      if (ors.size() == 1)
362         return ors.get(0);
363      return new Or(ors);
364   }
365}