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