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.common.utils.Utils.*;
020import static org.apache.juneau.internal.StateMachineState.*;
021
022import java.text.*;
023import java.util.*;
024import java.util.regex.*;
025
026import org.apache.juneau.common.utils.*;
027import org.apache.juneau.internal.*;
028
029/**
030 * Utility class for matching JEE user roles against string expressions.
031 *
032 * <p>
033 * Supports the following expression constructs:
034 * <ul>
035 *    <li><js>"foo"</js> - Single arguments.
036 *    <li><js>"foo,bar,baz"</js> - Multiple OR'ed arguments.
037 *    <li><js>"foo | bar | bqz"</js> - Multiple OR'ed arguments, pipe syntax.
038 *    <li><js>"foo || bar || bqz"</js> - Multiple OR'ed arguments, Java-OR syntax.
039 *    <li><js>"fo*"</js> - Patterns including <js>'*'</js> and <js>'?'</js>.
040 *    <li><js>"fo* &amp; *oo"</js> - Multiple AND'ed arguments, ampersand syntax.
041 *    <li><js>"fo* &amp;&amp; *oo"</js> - Multiple AND'ed arguments, Java-AND syntax.
042 *    <li><js>"fo* || (*oo || bar)"</js> - Parenthesis.
043 * </ul>
044 *
045 * <h5 class='section'>Notes:</h5><ul>
046 *    <li class='note'>AND operations take precedence over OR operations (as expected).
047 *    <li class='note'>Whitespace is ignored.
048 *    <li class='note'><jk>null</jk> or empty expressions always match as <jk>false</jk>.
049 * </ul>
050 *
051 * <h5 class='section'>See Also:</h5><ul>
052 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Guards">Guards</a>
053 * </ul>
054 */
055public class RoleMatcher {
056
057   private final Exp exp;
058   private static final AsciiSet
059      WS = AsciiSet.of(" \t"),
060      OP = AsciiSet.of(",|&"),
061      META = AsciiSet.of("*?");
062
063   /**
064    * Constructor.
065    *
066    * @param expression The string expression.
067    * @throws ParseException If the expression is malformed.
068    */
069   public RoleMatcher(String expression) throws ParseException {
070      this.exp = parse(expression);
071   }
072
073   /**
074    * Returns <jk>true</jk> if the specified string matches this expression.
075    *
076    * @param roles The user roles.
077    * @return
078    *    <jk>true</jk> if the specified string matches this expression.
079    *    <br>Always <jk>false</jk> if the string is <jk>null</jk>.
080    */
081   public boolean matches(Set<String> roles) {
082      return roles != null && exp.matches(roles);
083   }
084
085   @Override /* Object */
086   public String toString() {
087      return exp.toString();
088   }
089
090   /**
091    * Returns all the tokens used in this expression.
092    *
093    * @return All the tokens used in this expression.
094    */
095   public Set<String> getRolesInExpression() {
096      Set<String> set = new TreeSet<>();
097      exp.appendTokens(set);
098      return set;
099   }
100
101   private Exp parse(String expression) throws ParseException {
102      if (Utils.isEmptyOrBlank(expression))
103         return new Never();
104
105      expression = expression.trim();
106
107      List<Exp> ors = list();
108      List<Exp> ands = list();
109
110      StateMachineState state = S01;
111      int i = 0, mark = -1;
112      int pDepth = 0;
113      boolean error = false;
114
115      for (i = 0; i < expression.length(); i++) {
116         char c = expression.charAt(i);
117         if (state == S01) {
118            // S01 = Looking for start
119            if (! WS.contains(c)) {
120               if (c == '(') {
121                  state = S02;
122                  pDepth = 0;
123                  mark = i+1;
124               } else if (OP.contains(c)) {
125                  error = true;
126                  break;
127               } else {
128                  state = S03;
129                  mark = i;
130               }
131            }
132         } else if (state == S02) {
133            // S02 = Found [(], looking for [)].
134            if (c == '(')
135               pDepth++;
136            if (c == ')') {
137               if (pDepth > 0)
138                  pDepth--;
139               else {
140                  ands.add(parse(expression.substring(mark, i)));
141                  mark = -1;
142                  state = S04;
143               }
144            }
145         } else if (state == S03) {
146            // S03 = Found [A], looking for end of A.
147            if (WS.contains(c) || OP.contains(c)) {
148               ands.add(parseOperand(expression.substring(mark, i)));
149               mark = -1;
150               if (WS.contains(c)) {
151                  state = S04;
152               } else {
153                  i--;
154                  state = S05;
155               }
156            }
157         } else if (state == S04) {
158            // S04 = Found [A ], looking for & or | or ,.
159            if (! WS.contains(c)) {
160               if (OP.contains(c)) {
161                  i--;
162                  state = S05;
163               } else {
164                  error = true;
165                  break;
166               }
167            }
168         } else if (state == S05) {
169            // S05 = Found & or | or ,.
170            if (c == '&') {
171               //ands.add(operand);
172               state = S06;
173            } else /* (c == '|' || c == ',') */ {
174                if (ands.size() == 1) {
175                   ors.add(ands.get(0));
176                } else {
177                   ors.add(new And(ands));
178                }
179                ands.clear();
180                if (c == '|') {
181                   state = S07;
182                } else {
183                   state = S01;
184                }
185            }
186         } else if (state == S06) {
187            // S06 = Found &, looking for & or other
188            if (! WS.contains(c)) {
189               if (c != '&')
190                  i--;
191               state = S01;
192            }
193         } else /* (state == S07) */ {
194            // S07 = Found |, looking for | or other
195            if (! WS.contains(c)) {
196               if (c != '|')
197                  i--;
198               state = S01;
199            }
200         }
201      }
202
203      if (error)
204         throw new ParseException("Invalid character in expression '"+expression+"' at position " + i + ". state=" + state, i);
205
206      if (state == S01)
207         throw new ParseException("Could not find beginning of clause in '"+expression+"'", i);
208      if (state == S02)
209         throw new ParseException("Could not find matching parenthesis in expression '"+expression+"'", i);
210      if (state == S05 || state == S06 || state == S07)
211         throw new ParseException("Dangling clause in expression '"+expression+"'", i);
212
213      if (mark != -1)
214         ands.add(parseOperand(expression.substring(mark, expression.length())));
215      if (ands.size() == 1)
216         ors.add(ands.get(0));
217      else
218         ors.add(new And(ands));
219
220      if (ors.size() == 1)
221         return ors.get(0);
222      return new Or(ors);
223   }
224
225   private static Exp parseOperand(String operand) {
226      boolean hasMeta = false;
227      for (int i = 0; i < operand.length() && ! hasMeta; i++) {
228         char c = operand.charAt(i);
229         hasMeta |= META.contains(c);
230      }
231      return hasMeta ? new Match(operand) : new Eq(operand);
232   }
233
234   //-----------------------------------------------------------------------------------------------------------------
235   // Expression classes
236   //-----------------------------------------------------------------------------------------------------------------
237
238   abstract static class Exp {
239
240      abstract boolean matches(Set<String> roles);
241
242      void appendTokens(Set<String> set) {}
243   }
244
245   static class Never extends Exp {
246      @Override
247      boolean matches(Set<String> roles) {
248         return false;
249      }
250
251      @Override /* Object */
252      public String toString() {
253         return "(NEVER)";
254      }
255   }
256
257   static class And extends Exp {
258      Exp[] clauses;
259
260      And(List<Exp> clauses) {
261         this.clauses = clauses.toArray(new Exp[clauses.size()]);
262      }
263
264      @Override /* Exp */
265      boolean matches(Set<String> roles) {
266         for (Exp e : clauses)
267            if (! e.matches(roles))
268               return false;
269         return true;
270      }
271
272      @Override /* Exp */
273      void appendTokens(Set<String> set) {
274         for (Exp clause : clauses)
275            clause.appendTokens(set);
276      }
277
278      @Override /* Object */
279      public String toString() {
280         return "(& " + Utils.join(clauses, " ") + ')';
281      }
282   }
283
284   static class Or extends Exp {
285      Exp[] clauses;
286
287      Or(List<Exp> clauses) {
288         this.clauses = clauses.toArray(new Exp[clauses.size()]);
289      }
290
291      @Override
292      boolean matches(Set<String> roles) {
293         for (Exp e : clauses)
294            if (e.matches(roles))
295               return true;
296         return false;
297      }
298
299      @Override /* Exp */
300      void appendTokens(Set<String> set) {
301         for (Exp clause : clauses)
302            clause.appendTokens(set);
303      }
304
305      @Override /* Object */
306      public String toString() {
307         return "(| " + Utils.join(clauses, " ") + ')';
308      }
309   }
310
311   static class Eq extends Exp {
312      final String operand;
313
314      Eq(String operand) {
315         this.operand = operand;
316      }
317
318      @Override /* Exp */
319      boolean matches(Set<String> roles) {
320         for (String role : roles)
321            if (operand.equals(role))
322               return true;
323         return false;
324      }
325
326      @Override /* Exp */
327      void appendTokens(Set<String> set) {
328         set.add(operand);
329      }
330
331      @Override /* Object */
332      public String toString() {
333         return "[= " + operand + "]";
334      }
335   }
336
337   static class Match extends Exp {
338      final Pattern p;
339      final String operand;
340
341      Match(String operand) {
342         this.operand = operand;
343         p = Utils.getMatchPattern3(operand);
344      }
345
346      @Override /* Exp */
347      boolean matches(Set<String> roles) {
348         for (String role : roles)
349            if (p.matcher(role).matches())
350               return true;
351         return false;
352      }
353
354      @Override /* Exp */
355      void appendTokens(Set<String> set) {
356         set.add(operand);
357      }
358
359      @Override /* Object */
360      public String toString() {
361         return "[* " + p.pattern().replaceAll("\\\\[QE]", "") + "]";
362      }
363   }
364}