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