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