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 }