/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at * trunk/opends/resource/legal-notices/OpenDS.LICENSE * or https://OpenDS.dev.java.net/OpenDS.LICENSE. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable, * add the following below this CDDL HEADER, with the fields enclosed * by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2008 Sun Microsystems, Inc. */ package org.opends.server.authorization.dseecompat; import org.opends.messages.Message; import static org.opends.messages.AccessControlMessages.*; import static org.opends.server.authorization.dseecompat.Aci.*; import java.util.regex.Pattern; import java.util.regex.Matcher; import java.util.HashMap; /** * This class represents a single bind rule of an ACI permission-bind rule * pair. */ public class BindRule { /* * This hash table holds the keyword bind rule mapping. */ private HashMap<String, KeywordBindRule> keywordRuleMap = new HashMap<String, KeywordBindRule>(); /* * True is a boolean "not" was seen. */ private boolean negate=false; /* * Complex bind rules have left and right values. */ private BindRule left = null; private BindRule right = null; /* * Enumeration of the boolean type of the complex bind rule ("and" or "or"). */ private EnumBooleanTypes booleanType = null; /* * The keyword of a simple bind rule. */ private EnumBindRuleKeyword keyword = null; /* * Regular expression group position of a bind rule keyword. */ private static final int keywordPos = 1; /* * Regular expression group position of a bind rule operation. */ private static final int opPos = 2; /* * Regular expression group position of a bind rule expression. */ private static final int expressionPos = 3; /* * Regular expression group position of the remainder part of an operand. */ private static final int remainingOperandPos = 1; /* * Regular expression group position of the remainder of the bind rule. */ private static final int remainingBindrulePos = 2; /* * Regular expression for valid bind rule operator group. */ private static final String opRegGroup = "([!=<>]+)"; /* * Regular expression for the expression part of a partially parsed * bind rule. */ private static final String expressionRegex = "\"([^\"]+)\"" + ZERO_OR_MORE_WHITESPACE; /* * Regular expression for a single bind rule. */ private static final String bindruleRegex = WORD_GROUP_START_PATTERN + ZERO_OR_MORE_WHITESPACE + opRegGroup + ZERO_OR_MORE_WHITESPACE + expressionRegex; /* * Regular expression of the remainder part of a partially parsed bind rule. */ private static final String remainingBindruleRegex = ZERO_OR_MORE_WHITESPACE_START_PATTERN + WORD_GROUP + ZERO_OR_MORE_WHITESPACE + "(.*)$"; /** * Constructor that takes an keyword enumeration and corresponding * simple bind rule. The keyword string is the key for the keyword rule in * the keywordRuleMap. This is a simple bind rule representation: * keyword op rule * * An example of a simple bind rule is: * * userdn = "ldap:///anyone" * * @param keyword The keyword enumeration. * @param rule The rule corresponding to this keyword. */ private BindRule(EnumBindRuleKeyword keyword, KeywordBindRule rule) { this.keyword=keyword; this.keywordRuleMap.put(keyword.toString(), rule); } /* * TODO Verify that this handles the NOT boolean properly by * creating a unit test. * * I'm a bit confused by the constructor which takes left and right * arguments. Is it always supposed to have exactly two elements? * Is it supposed to keep nesting bind rules in a chain until all of * them have been processed? The documentation for this method needs * to be a lot clearer. Also, it doesn't look like it handles the NOT * type properly. */ /** * Constructor that represents a complex bind rule. The left and right * bind rules are saved along with the boolean type operator. A complex * bind rule looks like: * * bindrule booleantype bindrule * * Each side of the complex bind rule can be complex bind rule(s) * itself. An example of a complex bind rule would be: * * (dns="*.example.com" and (userdn="ldap:///anyone" or * (userdn="ldap:///cn=foo,dc=example,dc=com and ip=129.34.56.66))) * * This constructor should always have two elements. The processing * of a complex bind rule is dependent on the boolean operator type. * See the evalComplex method for more information. * * * @param left The bind rule left of the boolean. * @param right The right bind rule. * @param booleanType The boolean type enumeration ("and" or "or"). */ private BindRule(BindRule left, BindRule right, EnumBooleanTypes booleanType) { this.booleanType = booleanType; this.left = left; this.right = right; } /* * TODO Verify this method handles escaped parentheses by writing * a unit test. * * It doesn't look like the decode() method handles the possibility of * escaped parentheses in a bind rule. */ /** * Decode an ACI bind rule string representation. * @param input The string representation of the bind rule. * @return A BindRule class representing the bind rule. * @throws AciException If the string is an invalid bind rule. */ public static BindRule decode (String input) throws AciException { if ((input == null) || (input.length() == 0)) { return null; } String bindruleStr = input.trim(); char firstChar = bindruleStr.charAt(0); char[] bindruleArray = bindruleStr.toCharArray(); if (firstChar == '(') { BindRule bindrule_1 = null; int currentPos; int numOpen = 0; int numClose = 0; // Find the associated closed parenthesis for (currentPos = 0; currentPos < bindruleArray.length; currentPos++) { if (bindruleArray[currentPos] == '(') { numOpen++; } else if (bindruleArray[currentPos] == ')') { numClose++; } if (numClose == numOpen) { //We found the associated closed parenthesis //the parenthesis are removed String bindruleStr1 = bindruleStr.substring(1, currentPos); bindrule_1 = BindRule.decode(bindruleStr1); break; } } /* * Check that the number of open parenthesis is the same as * the number of closed parenthesis. * Raise an exception otherwise. */ if (numOpen > numClose) { Message message = ERR_ACI_SYNTAX_BIND_RULE_MISSING_CLOSE_PAREN.get(input); throw new AciException(message); } /* * If there are remaining chars => there MUST be an * operand (AND / OR) * otherwise there is a syntax error */ if (currentPos < (bindruleArray.length - 1)) { String remainingBindruleStr = bindruleStr.substring(currentPos + 1); return createBindRule(bindrule_1, remainingBindruleStr); } else { return bindrule_1; } } else { StringBuilder b=new StringBuilder(bindruleStr); /* * TODO Verify by unit test that this negation * is correct. This code handles a simple bind rule negation such * as: * * not userdn="ldap:///anyone" */ boolean negate=determineNegation(b); bindruleStr=b.toString(); Pattern bindrulePattern = Pattern.compile(bindruleRegex); Matcher bindruleMatcher = bindrulePattern.matcher(bindruleStr); int bindruleEndIndex; if (bindruleMatcher.find()) { bindruleEndIndex = bindruleMatcher.end(); BindRule bindrule_1 = parseAndCreateBindrule(bindruleMatcher); bindrule_1.setNegate(negate); if (bindruleEndIndex < bindruleStr.length()) { String remainingBindruleStr = bindruleStr.substring(bindruleEndIndex); return createBindRule(bindrule_1, remainingBindruleStr); } else { return bindrule_1; } } else { Message message = ERR_ACI_SYNTAX_INVALID_BIND_RULE_SYNTAX.get(input); throw new AciException(message); } } } /** * Parses a simple bind rule using the regular expression matcher. * @param bindruleMatcher A regular expression matcher holding * the engine to use in the creation of a simple bind rule. * @return A BindRule determined by the matcher. * @throws AciException If the bind rule matcher found errors. */ private static BindRule parseAndCreateBindrule(Matcher bindruleMatcher) throws AciException { String keywordStr = bindruleMatcher.group(keywordPos); String operatorStr = bindruleMatcher.group(opPos); String expression = bindruleMatcher.group(expressionPos); EnumBindRuleKeyword keyword; EnumBindRuleType operator; // Get the Keyword keyword = EnumBindRuleKeyword.createBindRuleKeyword(keywordStr); if (keyword == null) { Message message = WARN_ACI_SYNTAX_INVALID_BIND_RULE_KEYWORD.get(keywordStr); throw new AciException(message); } // Get the operator operator = EnumBindRuleType.createBindruleOperand(operatorStr); if (operator == null) { Message message = WARN_ACI_SYNTAX_INVALID_BIND_RULE_OPERATOR.get(operatorStr); throw new AciException(message); } //expression can't be null if (expression == null) { Message message = WARN_ACI_SYNTAX_MISSING_BIND_RULE_EXPRESSION.get(operatorStr); throw new AciException(message); } validateOperation(keyword, operator); KeywordBindRule rule = decode(expression, keyword, operator); return new BindRule(keyword, rule); } /** * Create a complex bind rule from a substring * parsed from the ACI string. * @param bindrule The left hand part of a complex bind rule * parsed previously. * @param remainingBindruleStr The string used to determine the right * hand part. * @return A BindRule representing a complex bind rule. * @throws AciException If the string contains an invalid * right hand bind rule string. */ private static BindRule createBindRule(BindRule bindrule, String remainingBindruleStr) throws AciException { Pattern remainingBindrulePattern = Pattern.compile(remainingBindruleRegex); Matcher remainingBindruleMatcher = remainingBindrulePattern.matcher(remainingBindruleStr); if (remainingBindruleMatcher.find()) { String remainingOperand = remainingBindruleMatcher.group(remainingOperandPos); String remainingBindrule = remainingBindruleMatcher.group(remainingBindrulePos); EnumBooleanTypes operand = EnumBooleanTypes.createBindruleOperand(remainingOperand); if ((operand == null) || ((operand != EnumBooleanTypes.AND_BOOLEAN_TYPE) && (operand != EnumBooleanTypes.OR_BOOLEAN_TYPE))) { Message message = WARN_ACI_SYNTAX_INVALID_BIND_RULE_BOOLEAN_OPERATOR .get(remainingOperand); throw new AciException(message); } StringBuilder ruleExpr=new StringBuilder(remainingBindrule); /* TODO write a unit test to verify. * This is a check for something like: * bindrule and not (bindrule) * or something ill-advised like: * and not not not (bindrule). */ boolean negate=determineNegation(ruleExpr); remainingBindrule=ruleExpr.toString(); BindRule bindrule_2 = BindRule.decode(remainingBindrule); bindrule_2.setNegate(negate); return new BindRule(bindrule, bindrule_2, operand); } else { Message message = ERR_ACI_SYNTAX_INVALID_BIND_RULE_SYNTAX.get( remainingBindruleStr); throw new AciException(message); } } /** * Tries to strip an "not" boolean modifier from the string and * determine at the same time if the value should be flipped. * For example: * * not not not bindrule * * is true. * * @param ruleExpr The bindrule expression to evaluate. This * string will be changed if needed. * @return True if the boolean needs to be negated. */ private static boolean determineNegation(StringBuilder ruleExpr) { boolean negate=false; String ruleStr=ruleExpr.toString(); while(ruleStr.regionMatches(true, 0, "not ", 0, 4)) { negate = !negate; ruleStr = ruleStr.substring(4); } ruleExpr.replace(0, ruleExpr.length(), ruleStr); return negate; } /** * Set the negation parameter as determined by the function above. * @param v The value to assign negate to. */ private void setNegate(boolean v) { negate=v; } /* * TODO This method needs to handle the userattr keyword. Also verify * that the rest of the keywords are handled correctly. * TODO Investigate moving this method into EnumBindRuleKeyword class. * * Does validateOperation need a default case? Why is USERATTR not in this * list? Why is TIMEOFDAY not in this list when DAYOFWEEK is in the list? * Would it be more appropriate to put this logic in the * EnumBindRuleKeyword class so we can be sure it's always handled properly * for all keywords? */ /** * Checks the keyword operator enumeration to make sure it is valid. * This method doesn't handle all cases. * @param keyword The keyword enumeration to evaluate. * @param op The operation enumeration to evaluate. * @throws AciException If the operation is not valid for the keyword. */ private static void validateOperation(EnumBindRuleKeyword keyword, EnumBindRuleType op) throws AciException { switch (keyword) { case USERDN: case ROLEDN: case GROUPDN: case IP: case DNS: case AUTHMETHOD: case DAYOFWEEK: if ((op != EnumBindRuleType.EQUAL_BINDRULE_TYPE) && (op != EnumBindRuleType.NOT_EQUAL_BINDRULE_TYPE)) { Message message = WARN_ACI_SYNTAX_INVALID_BIND_RULE_KEYWORD_OPERATOR_COMBO .get(keyword.toString(), op.toString()); throw new AciException(message); } } } /* * TODO Investigate moving into the EnumBindRuleKeyword class. * * Should we move the logic in the * decode(String,EnumBindRuleKeyword,EnumBindRuleType) method into the * EnumBindRuleKeyword class so we can be sure that it's always * handled properly for all keywords? */ /** * Creates a keyword bind rule suitable for saving in the keyword * rule map table. Each individual keyword class will do further * parsing and validation of the expression string. This processing * is part of the simple bind rule creation. * @param expr The expression string to further parse. * @param keyword The keyword to create. * @param op The operation part of the bind rule. * @return A keyword bind rule class that can be stored in the * map table. * @throws AciException If the expr string contains a invalid * bind rule. */ private static KeywordBindRule decode(String expr, EnumBindRuleKeyword keyword, EnumBindRuleType op) throws AciException { KeywordBindRule rule ; switch (keyword) { case USERDN: { rule = UserDN.decode(expr, op); break; } case ROLEDN: { //The roledn keyword is not supported. Throw an exception with //a message if it is seen in the ACI. Message message = WARN_ACI_SYNTAX_ROLEDN_NOT_SUPPORTED.get(expr); throw new AciException(message); } case GROUPDN: { rule = GroupDN.decode(expr, op); break; } case IP: { rule=IP.decode(expr, op); break; } case DNS: { rule = DNS.decode(expr, op); break; } case DAYOFWEEK: { rule = DayOfWeek.decode(expr, op); break; } case TIMEOFDAY: { rule=TimeOfDay.decode(expr, op); break; } case AUTHMETHOD: { rule = AuthMethod.decode(expr, op); break; } case USERATTR: { rule = UserAttr.decode(expr, op); break; } case SSF: { rule = SSF.decode(expr, op); break; } default: { Message message = WARN_ACI_SYNTAX_INVALID_BIND_RULE_KEYWORD.get( keyword.toString()); throw new AciException(message); } } return rule; } /** * Evaluate the results of a complex bind rule. If the boolean * is an AND type then left and right must be TRUE, else * it must be an OR result and one of the bind rules must be * TRUE. * @param left The left bind rule result to evaluate. * @param right The right bind result to evaluate. * @return The result of the complex evaluation. */ private EnumEvalResult evalComplex(EnumEvalResult left, EnumEvalResult right) { EnumEvalResult ret=EnumEvalResult.FALSE; if(booleanType == EnumBooleanTypes.AND_BOOLEAN_TYPE) { if((left == EnumEvalResult.TRUE) && (right == EnumEvalResult.TRUE)) ret=EnumEvalResult.TRUE; } else if((left == EnumEvalResult.TRUE) || (right == EnumEvalResult.TRUE)) ret=EnumEvalResult.TRUE; return ret; } /** * Evaluate an bind rule against an evaluation context. If it is a simple * bind rule (no boolean type) then grab the keyword rule from the map * table and call the corresponding evaluate function. If it is a * complex rule call the routine above "evalComplex()". * @param evalCtx The evaluation context to pass to the keyword * evaluation function. * @return An result enumeration containing the result of the evaluation. */ public EnumEvalResult evaluate(AciEvalContext evalCtx) { EnumEvalResult ret; //Simple bind rules have a null booleanType enumeration. if(this.booleanType == null) { KeywordBindRule rule=keywordRuleMap.get(keyword.toString()); ret = rule.evaluate(evalCtx); } else ret=evalComplex(left.evaluate(evalCtx),right.evaluate(evalCtx)); return EnumEvalResult.negateIfNeeded(ret, negate); } }