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