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* & *oo"</js> - Multiple AND'ed arguments, ampersand syntax. 037 * <li><js>"fo* && *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