/* * 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 legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * 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 legal-notices/CDDLv1_0.txt. * 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. * Portions Copyright 2013-2015 ForgeRock AS */ package org.opends.server.authorization.dseecompat; import static org.opends.messages.AccessControlMessages.*; import static org.opends.server.authorization.dseecompat.Aci.*; import java.util.HashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.forgerock.i18n.LocalizableMessage; /** * 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 final HashMap<String, KeywordBindRule> keywordRuleMap = new HashMap<>(); /** True is a boolean "not" was seen. */ private boolean negate; /** Complex bind rules have left and right values. */ private BindRule left; private BindRule right; /** Enumeration of the boolean type of the complex bind rule ("and" or "or"). */ private EnumBooleanTypes booleanType; /** The keyword of a simple bind rule. */ private EnumBindRuleKeyword keyword; /** 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) { throw new AciException(WARN_ACI_SYNTAX_BIND_RULE_MISSING_CLOSE_PAREN.get(input)); } /* * 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); } 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 { throw new AciException(WARN_ACI_SYNTAX_INVALID_BIND_RULE_SYNTAX.get(input)); } } } /** * 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); // Get the Keyword final EnumBindRuleKeyword keyword = EnumBindRuleKeyword.createBindRuleKeyword(keywordStr); if (keyword == null) { throw new AciException(WARN_ACI_SYNTAX_INVALID_BIND_RULE_KEYWORD.get(keywordStr)); } // Get the operator final EnumBindRuleType operator = EnumBindRuleType.createBindruleOperand(operatorStr); if (operator == null) { throw new AciException(WARN_ACI_SYNTAX_INVALID_BIND_RULE_OPERATOR.get(operatorStr)); } //expression can't be null if (expression == null) { throw new AciException(WARN_ACI_SYNTAX_MISSING_BIND_RULE_EXPRESSION.get(operatorStr)); } 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)) { LocalizableMessage 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); } throw new AciException(WARN_ACI_SYNTAX_INVALID_BIND_RULE_SYNTAX.get(remainingBindruleStr)); } /** * 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) { throw new AciException( WARN_ACI_SYNTAX_INVALID_BIND_RULE_KEYWORD_OPERATOR_COMBO.get(keyword, op)); } } } /* * 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 { switch (keyword) { case USERDN: return UserDN.decode(expr, op); case ROLEDN: //The roledn keyword is not supported. Throw an exception with //a message if it is seen in the ACI. throw new AciException(WARN_ACI_SYNTAX_ROLEDN_NOT_SUPPORTED.get(expr)); case GROUPDN: return GroupDN.decode(expr, op); case IP: return IP.decode(expr, op); case DNS: return DNS.decode(expr, op); case DAYOFWEEK: return DayOfWeek.decode(expr, op); case TIMEOFDAY: return TimeOfDay.decode(expr, op); case AUTHMETHOD: return AuthMethod.decode(expr, op); case USERATTR: return UserAttr.decode(expr, op); case SSF: return SSF.decode(expr, op); default: throw new AciException(WARN_ACI_SYNTAX_INVALID_BIND_RULE_KEYWORD.get(keyword)); } } /** * 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) { if (booleanType == EnumBooleanTypes.AND_BOOLEAN_TYPE) { if (left == EnumEvalResult.TRUE && right == EnumEvalResult.TRUE) { return EnumEvalResult.TRUE; } } else if (left == EnumEvalResult.TRUE || right == EnumEvalResult.TRUE) { return EnumEvalResult.TRUE; } return EnumEvalResult.FALSE; } /** * 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); } /** {@inheritDoc} */ @Override public String toString() { final StringBuilder sb = new StringBuilder(); toString(sb); return sb.toString(); } /** * Appends a string representation of this object to the provided buffer. * * @param buffer * The buffer into which a string representation of this object * should be appended. */ public final void toString(StringBuilder buffer) { if (this.keywordRuleMap != null) { for (KeywordBindRule rule : this.keywordRuleMap.values()) { rule.toString(buffer); buffer.append(";"); } } } }