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