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