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