| // *************************************************************************************************************************** |
| // * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * |
| // * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * |
| // * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * |
| // * with the License. You may obtain a copy of the License at * |
| // * * |
| // * http://www.apache.org/licenses/LICENSE-2.0 * |
| // * * |
| // * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * |
| // * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * |
| // * specific language governing permissions and limitations under the License. * |
| // *************************************************************************************************************************** |
| package org.apache.juneau.rest.guards; |
| |
| import java.text.*; |
| import java.util.*; |
| import java.util.regex.*; |
| |
| import org.apache.juneau.internal.*; |
| |
| import static org.apache.juneau.internal.StateMachineState.*; |
| |
| /** |
| * Utility class for matching JEE user roles against string expressions. |
| * |
| * <p> |
| * Supports the following expression constructs: |
| * <ul> |
| * <li><js>"foo"</js> - Single arguments. |
| * <li><js>"foo,bar,baz"</js> - Multiple OR'ed arguments. |
| * <li><js>"foo | bar | bqz"</js> - Multiple OR'ed arguments, pipe syntax. |
| * <li><js>"foo || bar || bqz"</js> - Multiple OR'ed arguments, Java-OR syntax. |
| * <li><js>"fo*"</js> - Patterns including <js>'*'</js> and <js>'?'</js>. |
| * <li><js>"fo* & *oo"</js> - Multiple AND'ed arguments, ampersand syntax. |
| * <li><js>"fo* && *oo"</js> - Multiple AND'ed arguments, Java-AND syntax. |
| * <li><js>"fo* || (*oo || bar)"</js> - Parenthesis. |
| * </ul> |
| * |
| * <ul class='notes'> |
| * <li>AND operations take precedence over OR operations (as expected). |
| * <li>Whitespace is ignored. |
| * <li><jk>null</jk> or empty expressions always match as <jk>false</jk>. |
| * </ul> |
| */ |
| public class RoleMatcher { |
| |
| private final Exp exp; |
| private static final AsciiSet |
| WS = AsciiSet.create(" \t"), |
| OP = AsciiSet.create(",|&"), |
| META = AsciiSet.create("*?"); |
| |
| /** |
| * Constructor. |
| * |
| * @param expression The string expression. |
| * @throws ParseException If the expression is malformed. |
| */ |
| public RoleMatcher(String expression) throws ParseException { |
| this.exp = parse(expression); |
| } |
| |
| /** |
| * Returns <jk>true</jk> if the specified string matches this expression. |
| * |
| * @param roles The user roles. |
| * @return |
| * <jk>true</jk> if the specified string matches this expression. |
| * <br>Always <jk>false</jk> if the string is <jk>null</jk>. |
| */ |
| public boolean matches(Set<String> roles) { |
| return roles != null && exp.matches(roles); |
| } |
| |
| @Override /* Object */ |
| public String toString() { |
| return exp.toString(); |
| } |
| |
| /** |
| * Returns all the tokens used in this expression. |
| * |
| * @return All the tokens used in this expression. |
| */ |
| public Set<String> getRolesInExpression() { |
| Set<String> set = new TreeSet<>(); |
| exp.appendTokens(set); |
| return set; |
| } |
| |
| private Exp parse(String expression) throws ParseException { |
| if (StringUtils.isEmptyOrBlank(expression)) |
| return new Never(); |
| |
| expression = expression.trim(); |
| |
| List<Exp> ors = new ArrayList<>(); |
| List<Exp> ands = new ArrayList<>(); |
| |
| StateMachineState state = S01; |
| int i = 0, mark = -1; |
| int pDepth = 0; |
| boolean error = false; |
| |
| for (i = 0; i < expression.length(); i++) { |
| char c = expression.charAt(i); |
| if (state == S01) { |
| // S01 = Looking for start |
| if (! WS.contains(c)) { |
| if (c == '(') { |
| state = S02; |
| pDepth = 0; |
| mark = i+1; |
| } else if (OP.contains(c)) { |
| error = true; |
| break; |
| } else { |
| state = S03; |
| mark = i; |
| } |
| } |
| } else if (state == S02) { |
| // S02 = Found [(], looking for [)]. |
| if (c == '(') |
| pDepth++; |
| if (c == ')') { |
| if (pDepth > 0) |
| pDepth--; |
| else { |
| ands.add(parse(expression.substring(mark, i))); |
| mark = -1; |
| state = S04; |
| } |
| } |
| } else if (state == S03) { |
| // S03 = Found [A], looking for end of A. |
| if (WS.contains(c) || OP.contains(c)) { |
| ands.add(parseOperand(expression.substring(mark, i))); |
| mark = -1; |
| if (WS.contains(c)) { |
| state = S04; |
| } else { |
| i--; |
| state = S05; |
| } |
| } |
| } else if (state == S04) { |
| // S04 = Found [A ], looking for & or | or ,. |
| if (! WS.contains(c)) { |
| if (OP.contains(c)) { |
| i--; |
| state = S05; |
| } else { |
| error = true; |
| break; |
| } |
| } |
| } else if (state == S05) { |
| // S05 = Found & or | or ,. |
| if (c == '&') { |
| //ands.add(operand); |
| state = S06; |
| } else /* (c == '|' || c == ',') */ { |
| if (ands.size() == 1) { |
| ors.add(ands.get(0)); |
| } else { |
| ors.add(new And(ands)); |
| } |
| ands.clear(); |
| if (c == '|') { |
| state = S07; |
| } else { |
| state = S01; |
| } |
| } |
| } else if (state == S06) { |
| // S06 = Found &, looking for & or other |
| if (! WS.contains(c)) { |
| if (c != '&') |
| i--; |
| state = S01; |
| } |
| } else /* (state == S07) */ { |
| // S07 = Found |, looking for | or other |
| if (! WS.contains(c)) { |
| if (c != '|') |
| i--; |
| state = S01; |
| } |
| } |
| } |
| |
| if (error) |
| throw new ParseException("Invalid character in expression '"+expression+"' at position " + i + ". state=" + state, i); |
| |
| if (state == S01) |
| throw new ParseException("Could not find beginning of clause in '"+expression+"'", i); |
| if (state == S02) |
| throw new ParseException("Could not find matching parenthesis in expression '"+expression+"'", i); |
| if (state == S05 || state == S06 || state == S07) |
| throw new ParseException("Dangling clause in expression '"+expression+"'", i); |
| |
| if (mark != -1) |
| ands.add(parseOperand(expression.substring(mark, expression.length()))); |
| if (ands.size() == 1) |
| ors.add(ands.get(0)); |
| else |
| ors.add(new And(ands)); |
| |
| if (ors.size() == 1) |
| return ors.get(0); |
| return new Or(ors); |
| } |
| |
| private static Exp parseOperand(String operand) { |
| boolean hasMeta = false; |
| for (int i = 0; i < operand.length() && ! hasMeta; i++) { |
| char c = operand.charAt(i); |
| hasMeta |= META.contains(c); |
| } |
| return hasMeta ? new Match(operand) : new Eq(operand); |
| } |
| |
| //----------------------------------------------------------------------------------------------------------------- |
| // Expression classes |
| //----------------------------------------------------------------------------------------------------------------- |
| |
| abstract static class Exp { |
| |
| abstract boolean matches(Set<String> roles); |
| |
| void appendTokens(Set<String> set) {} |
| } |
| |
| static class Never extends Exp { |
| @Override |
| boolean matches(Set<String> roles) { |
| return false; |
| } |
| |
| @Override /* Object */ |
| public String toString() { |
| return "(NEVER)"; |
| } |
| } |
| |
| static class And extends Exp { |
| Exp[] clauses; |
| |
| And(List<Exp> clauses) { |
| this.clauses = clauses.toArray(new Exp[clauses.size()]); |
| } |
| |
| @Override /* Exp */ |
| boolean matches(Set<String> roles) { |
| for (Exp e : clauses) |
| if (! e.matches(roles)) |
| return false; |
| return true; |
| } |
| |
| @Override /* Exp */ |
| void appendTokens(Set<String> set) { |
| for (Exp clause : clauses) |
| clause.appendTokens(set); |
| } |
| |
| @Override /* Object */ |
| public String toString() { |
| return "(& " + StringUtils.join(clauses, " ") + ')'; |
| } |
| } |
| |
| static class Or extends Exp { |
| Exp[] clauses; |
| |
| Or(List<Exp> clauses) { |
| this.clauses = clauses.toArray(new Exp[clauses.size()]); |
| } |
| |
| @Override |
| boolean matches(Set<String> roles) { |
| for (Exp e : clauses) |
| if (e.matches(roles)) |
| return true; |
| return false; |
| } |
| |
| @Override /* Exp */ |
| void appendTokens(Set<String> set) { |
| for (Exp clause : clauses) |
| clause.appendTokens(set); |
| } |
| |
| @Override /* Object */ |
| public String toString() { |
| return "(| " + StringUtils.join(clauses, " ") + ')'; |
| } |
| } |
| |
| static class Eq extends Exp { |
| final String operand; |
| |
| Eq(String operand) { |
| this.operand = operand; |
| } |
| |
| @Override /* Exp */ |
| boolean matches(Set<String> roles) { |
| for (String role : roles) |
| if (operand.equals(role)) |
| return true; |
| return false; |
| } |
| |
| @Override /* Exp */ |
| void appendTokens(Set<String> set) { |
| set.add(operand); |
| } |
| |
| @Override /* Object */ |
| public String toString() { |
| return "[= " + operand + "]"; |
| } |
| } |
| |
| static class Match extends Exp { |
| final Pattern p; |
| final String operand; |
| |
| Match(String operand) { |
| this.operand = operand; |
| p = StringUtils.getMatchPattern(operand); |
| } |
| |
| @Override /* Exp */ |
| boolean matches(Set<String> roles) { |
| for (String role : roles) |
| if (p.matcher(role).matches()) |
| return true; |
| return false; |
| } |
| |
| @Override /* Exp */ |
| void appendTokens(Set<String> set) { |
| set.add(operand); |
| } |
| |
| @Override /* Object */ |
| public String toString() { |
| return "[* " + p.pattern().replaceAll("\\\\[QE]", "") + "]"; |
| } |
| } |
| } |
| |