/*
|
* 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-2014 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<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) {
|
LocalizableMessage 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 {
|
LocalizableMessage 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)
|
{
|
LocalizableMessage message =
|
WARN_ACI_SYNTAX_INVALID_BIND_RULE_KEYWORD.get(keywordStr);
|
throw new AciException(message);
|
}
|
|
// Get the operator
|
operator = EnumBindRuleType.createBindruleOperand(operatorStr);
|
if (operator == null) {
|
LocalizableMessage message =
|
WARN_ACI_SYNTAX_INVALID_BIND_RULE_OPERATOR.get(operatorStr);
|
throw new AciException(message);
|
}
|
|
//expression can't be null
|
if (expression == null) {
|
LocalizableMessage 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))) {
|
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);
|
} else {
|
LocalizableMessage 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)) {
|
LocalizableMessage 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.
|
LocalizableMessage 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: {
|
LocalizableMessage 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);
|
}
|
|
/** {@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(";");
|
}
|
}
|
}
|
}
|