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