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.commons.lang.StateEnum.*; 020import static org.apache.juneau.commons.utils.CollectionUtils.*; 021import static org.apache.juneau.commons.utils.StringUtils.*; 022import static org.apache.juneau.commons.utils.Utils.*; 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 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 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 (operand.equals(role)) 107 return true; 108 return false; 109 } 110 } 111 112 abstract static class Exp { 113 114 void appendTokens(Set<String> set) {} 115 116 abstract boolean matches(Set<String> roles); 117 } 118 119 static class Match extends Exp { 120 final Pattern p; 121 final String operand; 122 123 Match(String operand) { 124 this.operand = operand; 125 p = getMatchPattern(operand); 126 } 127 128 @Override /* Overridden from Object */ 129 public String toString() { 130 return "[* " + p.pattern().replaceAll("\\\\[QE]", "") + "]"; 131 } 132 133 @Override /* Overridden from Exp */ 134 void appendTokens(Set<String> set) { 135 set.add(operand); 136 } 137 138 @Override /* Overridden from Exp */ 139 boolean matches(Set<String> roles) { 140 for (var role : roles) 141 if (p.matcher(role).matches()) 142 return true; 143 return false; 144 } 145 } 146 147 static class Never extends Exp { 148 @Override /* Overridden from Object */ 149 public String toString() { 150 return "(NEVER)"; 151 } 152 153 @Override 154 boolean matches(Set<String> roles) { 155 return false; 156 } 157 } 158 159 static class Or extends Exp { 160 Exp[] clauses; 161 162 Or(List<Exp> clauses) { 163 this.clauses = clauses.toArray(new Exp[clauses.size()]); 164 } 165 166 @Override /* Overridden from Object */ 167 public String toString() { 168 return "(| " + join(clauses, " ") + ')'; 169 } 170 171 @Override /* Overridden from Exp */ 172 void appendTokens(Set<String> set) { 173 for (var clause : clauses) 174 clause.appendTokens(set); 175 } 176 177 @Override 178 boolean matches(Set<String> roles) { 179 for (var e : clauses) 180 if (e.matches(roles)) 181 return true; 182 return false; 183 } 184 } 185 186 // @formatter:off 187 private static final AsciiSet 188 WS = AsciiSet.of(" \t"), 189 OP = AsciiSet.of(",|&"), 190 META = AsciiSet.of("*?"); 191 // @formatter:on 192 193 private static Exp parseOperand(String operand) { 194 boolean hasMeta = false; 195 for (var i = 0; i < operand.length() && ! hasMeta; i++) { 196 var c = operand.charAt(i); 197 hasMeta |= META.contains(c); 198 } 199 return hasMeta ? new Match(operand) : new Eq(operand); 200 } 201 202 private final Exp exp; 203 204 /** 205 * Constructor. 206 * 207 * @param expression The string expression. 208 * @throws ParseException If the expression is malformed. 209 */ 210 public RoleMatcher(String expression) throws ParseException { 211 this.exp = parse(expression); 212 } 213 214 /** 215 * Returns all the tokens used in this expression. 216 * 217 * @return All the tokens used in this expression. 218 */ 219 public Set<String> getRolesInExpression() { 220 var set = new TreeSet<String>(); 221 exp.appendTokens(set); 222 return set; 223 } 224 225 /** 226 * Returns <jk>true</jk> if the specified string matches this expression. 227 * 228 * @param roles The user roles. 229 * @return 230 * <jk>true</jk> if the specified string matches this expression. 231 * <br>Always <jk>false</jk> if the string is <jk>null</jk>. 232 */ 233 public boolean matches(Set<String> roles) { 234 return nn(roles) && exp.matches(roles); 235 } 236 237 @Override /* Overridden from Object */ 238 public String toString() { 239 return exp.toString(); 240 } 241 242 private Exp parse(String expression) throws ParseException { 243 if (StringUtils.isBlank(expression)) 244 return new Never(); 245 246 expression = expression.trim(); 247 248 List<Exp> ors = list(); 249 List<Exp> ands = list(); 250 251 StateEnum state = S1; 252 int i = 0, mark = -1; 253 int pDepth = 0; 254 boolean error = false; 255 256 for (i = 0; i < expression.length(); i++) { 257 var c = expression.charAt(i); 258 if (state == S1) { 259 // S1 = Looking for start 260 if (! WS.contains(c)) { 261 if (c == '(') { 262 state = S2; 263 pDepth = 0; 264 mark = i + 1; 265 } else if (OP.contains(c)) { 266 error = true; 267 break; 268 } else { 269 state = S3; 270 mark = i; 271 } 272 } 273 } else if (state == S2) { 274 // S2 = Found [(], looking for [)]. 275 if (c == '(') 276 pDepth++; 277 if (c == ')') { 278 if (pDepth > 0) 279 pDepth--; 280 else { 281 ands.add(parse(expression.substring(mark, i))); 282 mark = -1; 283 state = S4; 284 } 285 } 286 } else if (state == S3) { 287 // S3 = Found [A], looking for end of A. 288 if (WS.contains(c) || OP.contains(c)) { 289 ands.add(parseOperand(expression.substring(mark, i))); 290 mark = -1; 291 if (WS.contains(c)) { 292 state = S4; 293 } else { 294 i--; 295 state = S5; 296 } 297 } 298 } else if (state == S4) { 299 // S4 = Found [A ], looking for & or | or ,. 300 if (! WS.contains(c)) { 301 if (OP.contains(c)) { 302 i--; 303 state = S5; 304 } else { 305 error = true; 306 break; 307 } 308 } 309 } else if (state == S5) { 310 // S5 = Found & or | or ,. 311 if (c == '&') { 312 //ands.add(operand); 313 state = S6; 314 } else /* (c == '|' || c == ',') */ { 315 if (ands.size() == 1) { 316 ors.add(ands.get(0)); 317 } else { 318 ors.add(new And(ands)); 319 } 320 ands.clear(); 321 if (c == '|') { 322 state = S7; 323 } else { 324 state = S1; 325 } 326 } 327 } else if (state == S6) { 328 // S6 = Found &, looking for & or other 329 if (! WS.contains(c)) { 330 if (c != '&') 331 i--; 332 state = S1; 333 } 334 } else /* (state == S7) */ { 335 // S7 = Found |, looking for | or other 336 if (! WS.contains(c)) { 337 if (c != '|') 338 i--; 339 state = S1; 340 } 341 } 342 } 343 344 if (error) 345 throw new ParseException("Invalid character in expression '" + expression + "' at position " + i + ". state=" + state, i); 346 347 if (state == S1) 348 throw new ParseException("Could not find beginning of clause in '" + expression + "'", i); 349 if (state == S2) 350 throw new ParseException("Could not find matching parenthesis in expression '" + expression + "'", i); 351 if (state == S5 || state == S6 || state == S7) 352 throw new ParseException("Dangling clause in expression '" + expression + "'", i); 353 354 if (mark != -1) 355 ands.add(parseOperand(expression.substring(mark, expression.length()))); 356 if (ands.size() == 1) 357 ors.add(ands.get(0)); 358 else 359 ors.add(new And(ands)); 360 361 if (ors.size() == 1) 362 return ors.get(0); 363 return new Or(ors); 364 } 365}