001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2008 Sun Microsystems, Inc. 015 * Portions Copyright 2013-2016 ForgeRock AS. 016 */ 017package org.opends.server.authorization.dseecompat; 018 019import static org.opends.messages.AccessControlMessages.*; 020import static org.opends.server.authorization.dseecompat.Aci.*; 021 022import java.util.HashMap; 023import java.util.Map; 024import java.util.regex.Matcher; 025import java.util.regex.Pattern; 026 027import org.forgerock.i18n.LocalizableMessage; 028 029/** This class represents a single bind rule of an ACI permission-bind rule pair. */ 030public class BindRule { 031 /** This hash table holds the keyword bind rule mapping. */ 032 private final Map<String, KeywordBindRule> keywordRuleMap = new HashMap<>(); 033 034 /** True is a boolean "not" was seen. */ 035 private boolean negate; 036 037 /** Complex bind rules have left and right values. */ 038 private final BindRule left; 039 private final BindRule right; 040 041 /** Enumeration of the boolean type of the complex bind rule ("and" or "or"). */ 042 private final EnumBooleanTypes booleanType; 043 /** The keyword of a simple bind rule. */ 044 private final EnumBindRuleKeyword keyword; 045 046 /** Regular expression group position of a bind rule keyword. */ 047 private static final int keywordPos = 1; 048 /** Regular expression group position of a bind rule operation. */ 049 private static final int opPos = 2; 050 /** Regular expression group position of a bind rule expression. */ 051 private static final int expressionPos = 3; 052 /** Regular expression group position of the remainder part of an operand. */ 053 private static final int remainingOperandPos = 1; 054 /** Regular expression group position of the remainder of the bind rule. */ 055 private static final int remainingBindrulePos = 2; 056 057 /** Regular expression for valid bind rule operator group. */ 058 private static final String opRegGroup = "([!=<>]+)"; 059 060 /** Regular expression for the expression part of a partially parsed bind rule. */ 061 private static final String expressionRegex = "\"([^\"]+)\"" + ZERO_OR_MORE_WHITESPACE; 062 063 /** Regular expression for a single bind rule. */ 064 private static final String bindruleRegex = 065 WORD_GROUP_START_PATTERN + ZERO_OR_MORE_WHITESPACE + 066 opRegGroup + ZERO_OR_MORE_WHITESPACE + expressionRegex; 067 068 /** Regular expression of the remainder part of a partially parsed bind rule. */ 069 private static final String remainingBindruleRegex = 070 ZERO_OR_MORE_WHITESPACE_START_PATTERN + WORD_GROUP + 071 ZERO_OR_MORE_WHITESPACE + "(.*)$"; 072 073 /** 074 * Constructor that takes an keyword enumeration and corresponding 075 * simple bind rule. The keyword string is the key for the keyword rule in 076 * the keywordRuleMap. This is a simple bind rule representation: 077 078 * keyword op rule 079 * 080 * An example of a simple bind rule is: 081 * 082 * userdn = "ldap:///anyone" 083 * 084 * @param keyword The keyword enumeration. 085 * @param rule The rule corresponding to this keyword. 086 */ 087 private BindRule(EnumBindRuleKeyword keyword, KeywordBindRule rule) { 088 this.keyword=keyword; 089 this.keywordRuleMap.put(keyword.toString(), rule); 090 this.booleanType = null; 091 this.left = null; 092 this.right = null; 093 } 094 095 /* 096 * TODO Verify that this handles the NOT boolean properly by 097 * creating a unit test. 098 * 099 * I'm a bit confused by the constructor which takes left and right 100 * arguments. Is it always supposed to have exactly two elements? 101 * Is it supposed to keep nesting bind rules in a chain until all of 102 * them have been processed? The documentation for this method needs 103 * to be a lot clearer. Also, it doesn't look like it handles the NOT 104 * type properly. 105 */ 106 /** 107 * Constructor that represents a complex bind rule. The left and right 108 * bind rules are saved along with the boolean type operator. A complex 109 * bind rule looks like: 110 * 111 * bindrule booleantype bindrule 112 * 113 * Each side of the complex bind rule can be complex bind rule(s) 114 * itself. An example of a complex bind rule would be: 115 * 116 * (dns="*.example.com" and (userdn="ldap:///anyone" or 117 * (userdn="ldap:///cn=foo,dc=example,dc=com and ip=129.34.56.66))) 118 * 119 * This constructor should always have two elements. The processing 120 * of a complex bind rule is dependent on the boolean operator type. 121 * See the evalComplex method for more information. 122 * 123 * 124 * @param left The bind rule left of the boolean. 125 * @param right The right bind rule. 126 * @param booleanType The boolean type enumeration ("and" or "or"). 127 */ 128 private BindRule(BindRule left, BindRule right, EnumBooleanTypes booleanType) { 129 this.keyword = null; 130 this.booleanType = booleanType; 131 this.left = left; 132 this.right = right; 133 } 134 135 /* 136 * TODO Verify this method handles escaped parentheses by writing 137 * a unit test. 138 * 139 * It doesn't look like the decode() method handles the possibility of 140 * escaped parentheses in a bind rule. 141 */ 142 /** 143 * Decode an ACI bind rule string representation. 144 * @param input The string representation of the bind rule. 145 * @return A BindRule class representing the bind rule. 146 * @throws AciException If the string is an invalid bind rule. 147 */ 148 public static BindRule decode (String input) throws AciException { 149 if (input == null || input.length() == 0) 150 { 151 return null; 152 } 153 String bindruleStr = input.trim(); 154 char firstChar = bindruleStr.charAt(0); 155 char[] bindruleArray = bindruleStr.toCharArray(); 156 157 if (firstChar == '(') 158 { 159 BindRule bindrule_1 = null; 160 int currentPos; 161 int numOpen = 0; 162 int numClose = 0; 163 164 // Find the associated closed parenthesis 165 for (currentPos = 0; currentPos < bindruleArray.length; currentPos++) 166 { 167 if (bindruleArray[currentPos] == '(') 168 { 169 numOpen++; 170 } 171 else if (bindruleArray[currentPos] == ')') 172 { 173 numClose++; 174 } 175 if (numClose == numOpen) 176 { 177 // We found the associated closed parenthesis the parenthesis are removed 178 String bindruleStr1 = bindruleStr.substring(1, currentPos); 179 bindrule_1 = BindRule.decode(bindruleStr1); 180 break; 181 } 182 } 183 /* 184 * Check that the number of open parenthesis is the same as 185 * the number of closed parenthesis. 186 * Raise an exception otherwise. 187 */ 188 if (numOpen > numClose) { 189 throw new AciException(WARN_ACI_SYNTAX_BIND_RULE_MISSING_CLOSE_PAREN.get(input)); 190 } 191 /* 192 * If there are remaining chars => there MUST be an operand (AND / OR) 193 * otherwise there is a syntax error 194 */ 195 if (currentPos < bindruleArray.length - 1) 196 { 197 String remainingBindruleStr = 198 bindruleStr.substring(currentPos + 1); 199 return createBindRule(bindrule_1, remainingBindruleStr); 200 } 201 return bindrule_1; 202 } 203 else 204 { 205 StringBuilder b=new StringBuilder(bindruleStr); 206 /* 207 * TODO Verify by unit test that this negation 208 * is correct. This code handles a simple bind rule negation such as: 209 * 210 * not userdn="ldap:///anyone" 211 */ 212 boolean negate=determineNegation(b); 213 bindruleStr=b.toString(); 214 Pattern bindrulePattern = Pattern.compile(bindruleRegex); 215 Matcher bindruleMatcher = bindrulePattern.matcher(bindruleStr); 216 int bindruleEndIndex; 217 if (bindruleMatcher.find()) 218 { 219 bindruleEndIndex = bindruleMatcher.end(); 220 BindRule bindrule_1 = parseAndCreateBindrule(bindruleMatcher); 221 bindrule_1.setNegate(negate); 222 if (bindruleEndIndex < bindruleStr.length()) 223 { 224 String remainingBindruleStr = bindruleStr.substring(bindruleEndIndex); 225 return createBindRule(bindrule_1, remainingBindruleStr); 226 } 227 else { 228 return bindrule_1; 229 } 230 } 231 else { 232 throw new AciException(WARN_ACI_SYNTAX_INVALID_BIND_RULE_SYNTAX.get(input)); 233 } 234 } 235 } 236 237 /** 238 * Parses a simple bind rule using the regular expression matcher. 239 * @param bindruleMatcher A regular expression matcher holding 240 * the engine to use in the creation of a simple bind rule. 241 * @return A BindRule determined by the matcher. 242 * @throws AciException If the bind rule matcher found errors. 243 */ 244 private static BindRule parseAndCreateBindrule(Matcher bindruleMatcher) throws AciException { 245 String keywordStr = bindruleMatcher.group(keywordPos); 246 String operatorStr = bindruleMatcher.group(opPos); 247 String expression = bindruleMatcher.group(expressionPos); 248 249 // Get the Keyword 250 final EnumBindRuleKeyword keyword = EnumBindRuleKeyword.createBindRuleKeyword(keywordStr); 251 if (keyword == null) 252 { 253 throw new AciException(WARN_ACI_SYNTAX_INVALID_BIND_RULE_KEYWORD.get(keywordStr)); 254 } 255 256 // Get the operator 257 final EnumBindRuleType operator = EnumBindRuleType.createBindruleOperand(operatorStr); 258 if (operator == null) { 259 throw new AciException(WARN_ACI_SYNTAX_INVALID_BIND_RULE_OPERATOR.get(operatorStr)); 260 } 261 262 //expression can't be null 263 if (expression == null) { 264 throw new AciException(WARN_ACI_SYNTAX_MISSING_BIND_RULE_EXPRESSION.get(operatorStr)); 265 } 266 validateOperation(keyword, operator); 267 KeywordBindRule rule = decode(expression, keyword, operator); 268 return new BindRule(keyword, rule); 269 } 270 271 /** 272 * Create a complex bind rule from a substring 273 * parsed from the ACI string. 274 * @param bindrule The left hand part of a complex bind rule 275 * parsed previously. 276 * @param remainingBindruleStr The string used to determine the right 277 * hand part. 278 * @return A BindRule representing a complex bind rule. 279 * @throws AciException If the string contains an invalid 280 * right hand bind rule string. 281 */ 282 private static BindRule createBindRule(BindRule bindrule, 283 String remainingBindruleStr) throws AciException { 284 Pattern remainingBindrulePattern = Pattern.compile(remainingBindruleRegex); 285 Matcher remainingBindruleMatcher = remainingBindrulePattern.matcher(remainingBindruleStr); 286 if (remainingBindruleMatcher.find()) { 287 String remainingOperand = remainingBindruleMatcher.group(remainingOperandPos); 288 String remainingBindrule = remainingBindruleMatcher.group(remainingBindrulePos); 289 EnumBooleanTypes operand = EnumBooleanTypes.createBindruleOperand(remainingOperand); 290 if (operand == null 291 || (operand != EnumBooleanTypes.AND_BOOLEAN_TYPE 292 && operand != EnumBooleanTypes.OR_BOOLEAN_TYPE)) { 293 LocalizableMessage message = 294 WARN_ACI_SYNTAX_INVALID_BIND_RULE_BOOLEAN_OPERATOR.get(remainingOperand); 295 throw new AciException(message); 296 } 297 StringBuilder ruleExpr=new StringBuilder(remainingBindrule); 298 /* TODO write a unit test to verify. 299 * This is a check for something like: 300 * bindrule and not (bindrule) 301 * or something ill-advised like: 302 * and not not not (bindrule). 303 */ 304 boolean negate=determineNegation(ruleExpr); 305 remainingBindrule=ruleExpr.toString(); 306 BindRule bindrule_2 = BindRule.decode(remainingBindrule); 307 bindrule_2.setNegate(negate); 308 return new BindRule(bindrule, bindrule_2, operand); 309 } 310 throw new AciException(WARN_ACI_SYNTAX_INVALID_BIND_RULE_SYNTAX.get(remainingBindruleStr)); 311 } 312 313 /** 314 * Tries to strip an "not" boolean modifier from the string and 315 * determine at the same time if the value should be flipped. 316 * For example: 317 * 318 * not not not bindrule 319 * 320 * is true. 321 * 322 * @param ruleExpr The bindrule expression to evaluate. This 323 * string will be changed if needed. 324 * @return True if the boolean needs to be negated. 325 */ 326 private static boolean determineNegation(StringBuilder ruleExpr) { 327 boolean negate=false; 328 String ruleStr=ruleExpr.toString(); 329 while(ruleStr.regionMatches(true, 0, "not ", 0, 4)) { 330 negate = !negate; 331 ruleStr = ruleStr.substring(4); 332 } 333 ruleExpr.replace(0, ruleExpr.length(), ruleStr); 334 return negate; 335 } 336 337 /** 338 * Set the negation parameter as determined by the function above. 339 * @param v The value to assign negate to. 340 */ 341 private void setNegate(boolean v) { 342 negate=v; 343 } 344 345 /* 346 * TODO This method needs to handle the userattr keyword. Also verify 347 * that the rest of the keywords are handled correctly. 348 * TODO Investigate moving this method into EnumBindRuleKeyword class. 349 * 350 * Does validateOperation need a default case? Why is USERATTR not in this 351 * list? Why is TIMEOFDAY not in this list when DAYOFWEEK is in the list? 352 * Would it be more appropriate to put this logic in the 353 * EnumBindRuleKeyword class so we can be sure it's always handled properly 354 * for all keywords? 355 */ 356 /** 357 * Checks the keyword operator enumeration to make sure it is valid. 358 * This method doesn't handle all cases. 359 * @param keyword The keyword enumeration to evaluate. 360 * @param op The operation enumeration to evaluate. 361 * @throws AciException If the operation is not valid for the keyword. 362 */ 363 private static void validateOperation(EnumBindRuleKeyword keyword, 364 EnumBindRuleType op) 365 throws AciException { 366 switch (keyword) { 367 case USERDN: 368 case ROLEDN: 369 case GROUPDN: 370 case IP: 371 case DNS: 372 case AUTHMETHOD: 373 case DAYOFWEEK: 374 if (op != EnumBindRuleType.EQUAL_BINDRULE_TYPE 375 && op != EnumBindRuleType.NOT_EQUAL_BINDRULE_TYPE) { 376 throw new AciException( 377 WARN_ACI_SYNTAX_INVALID_BIND_RULE_KEYWORD_OPERATOR_COMBO.get(keyword, op)); 378 } 379 } 380 } 381 382 /* 383 * TODO Investigate moving into the EnumBindRuleKeyword class. 384 * 385 * Should we move the logic in the 386 * decode(String,EnumBindRuleKeyword,EnumBindRuleType) method into the 387 * EnumBindRuleKeyword class so we can be sure that it's always 388 * handled properly for all keywords? 389 */ 390 /** 391 * Creates a keyword bind rule suitable for saving in the keyword 392 * rule map table. Each individual keyword class will do further 393 * parsing and validation of the expression string. This processing 394 * is part of the simple bind rule creation. 395 * @param expr The expression string to further parse. 396 * @param keyword The keyword to create. 397 * @param op The operation part of the bind rule. 398 * @return A keyword bind rule class that can be stored in the 399 * map table. 400 * @throws AciException If the expr string contains a invalid 401 * bind rule. 402 */ 403 private static KeywordBindRule decode(String expr, EnumBindRuleKeyword keyword, EnumBindRuleType op) 404 throws AciException { 405 switch (keyword) { 406 case USERDN: 407 return UserDN.decode(expr, op); 408 case ROLEDN: 409 //The roledn keyword is not supported. Throw an exception with 410 //a message if it is seen in the ACI. 411 throw new AciException(WARN_ACI_SYNTAX_ROLEDN_NOT_SUPPORTED.get(expr)); 412 case GROUPDN: 413 return GroupDN.decode(expr, op); 414 case IP: 415 return IP.decode(expr, op); 416 case DNS: 417 return DNS.decode(expr, op); 418 case DAYOFWEEK: 419 return DayOfWeek.decode(expr, op); 420 case TIMEOFDAY: 421 return TimeOfDay.decode(expr, op); 422 case AUTHMETHOD: 423 return AuthMethod.decode(expr, op); 424 case USERATTR: 425 return UserAttr.decode(expr, op); 426 case SSF: 427 return SSF.decode(expr, op); 428 default: 429 throw new AciException(WARN_ACI_SYNTAX_INVALID_BIND_RULE_KEYWORD.get(keyword)); 430 } 431 } 432 433 /** 434 * Evaluate the results of a complex bind rule. If the boolean 435 * is an AND type then left and right must be TRUE, else 436 * it must be an OR result and one of the bind rules must be 437 * TRUE. 438 * @param left The left bind rule result to evaluate. 439 * @param right The right bind result to evaluate. 440 * @return The result of the complex evaluation. 441 */ 442 private EnumEvalResult evalComplex(EnumEvalResult left, EnumEvalResult right) { 443 if (booleanType == EnumBooleanTypes.AND_BOOLEAN_TYPE) { 444 if (left == EnumEvalResult.TRUE && right == EnumEvalResult.TRUE) { 445 return EnumEvalResult.TRUE; 446 } 447 } else if (left == EnumEvalResult.TRUE || right == EnumEvalResult.TRUE) { 448 return EnumEvalResult.TRUE; 449 } 450 return EnumEvalResult.FALSE; 451 } 452 453 /** 454 * Evaluate an bind rule against an evaluation context. If it is a simple 455 * bind rule (no boolean type) then grab the keyword rule from the map 456 * table and call the corresponding evaluate function. If it is a 457 * complex rule call the routine above "evalComplex()". 458 * @param evalCtx The evaluation context to pass to the keyword 459 * evaluation function. 460 * @return An result enumeration containing the result of the evaluation. 461 */ 462 public EnumEvalResult evaluate(AciEvalContext evalCtx) { 463 EnumEvalResult ret; 464 //Simple bind rules have a null booleanType enumeration. 465 if(this.booleanType == null) { 466 KeywordBindRule rule=keywordRuleMap.get(keyword.toString()); 467 ret = rule.evaluate(evalCtx); 468 } else { 469 ret = evalComplex(left.evaluate(evalCtx),right.evaluate(evalCtx)); 470 } 471 return EnumEvalResult.negateIfNeeded(ret, negate); 472 } 473 474 @Override 475 public String toString() { 476 final StringBuilder sb = new StringBuilder(); 477 toString(sb); 478 return sb.toString(); 479 } 480 481 /** 482 * Appends a string representation of this object to the provided buffer. 483 * 484 * @param buffer 485 * The buffer into which a string representation of this object 486 * should be appended. 487 */ 488 public final void toString(StringBuilder buffer) { 489 if (this.keywordRuleMap != null) { 490 for (KeywordBindRule rule : this.keywordRuleMap.values()) { 491 rule.toString(buffer); 492 buffer.append(";"); 493 } 494 } 495 } 496}