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