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-2010 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.regex.Matcher;
033import java.util.regex.Pattern;
034
035import org.forgerock.i18n.LocalizableMessage;
036import org.opends.server.types.AttributeType;
037import org.opends.server.types.DN;
038import org.forgerock.opendj.ldap.SearchScope;
039
040/**
041 * This class represents target part of an ACI's syntax. This is the part
042 * of an ACI before the ACI body and specifies the entry, attributes, or set
043 * of entries and attributes which the ACI controls access.
044 *
045 * The supported  ACI target keywords are: target, targetattr,
046 * targetscope, targetfilter, targattrfilters, targetcontrol and extop.
047 */
048public class AciTargets {
049
050    /**
051     * ACI syntax has a target keyword.
052     */
053    private Target target;
054
055    /**
056     * ACI syntax has a targetscope keyword.
057     */
058    private SearchScope targetScope = SearchScope.WHOLE_SUBTREE;
059
060    /**
061     * ACI syntax has a targetattr keyword.
062     */
063    private TargetAttr targetAttr;
064
065    /**
066     * ACI syntax has a targetfilter keyword.
067     */
068    private TargetFilter targetFilter;
069
070    /**
071     * ACI syntax has a targattrtfilters keyword.
072     */
073    private TargAttrFilters targAttrFilters;
074
075   /**
076    * The ACI syntax has a targetcontrol keyword.
077    */
078    private TargetControl targetControl;
079
080   /**
081    * The ACI syntax has a extop keyword.
082    */
083    private ExtOp extOp;
084
085    /**
086     * The number of regular expression group positions in a valid ACI target
087     * expression.
088     */
089    private static final int targetElementCount = 3;
090
091    /**
092     *  Regular expression group position of a target keyword.
093     */
094    private static final int targetKeywordPos       = 1;
095
096    /**
097     *  Regular expression group position of a target operator enumeration.
098     */
099    private static final int targetOperatorPos      = 2;
100
101    /**
102     *  Regular expression group position of a target expression statement.
103     */
104    private static final int targetExpressionPos    = 3;
105
106    /**
107     * Regular expression used to match a single target rule.
108     */
109    private static final String targetRegex =
110           OPEN_PAREN +  ZERO_OR_MORE_WHITESPACE  +  WORD_GROUP +
111           ZERO_OR_MORE_WHITESPACE + "(!?=)" + ZERO_OR_MORE_WHITESPACE +
112           "\"([^\"]+)\"" + ZERO_OR_MORE_WHITESPACE + CLOSED_PAREN +
113           ZERO_OR_MORE_WHITESPACE;
114
115    /**
116     * Regular expression used to match one or more target rules. The pattern is
117     * part of a general ACI verification.
118     */
119    public static final String targetsRegex = "(" + targetRegex + ")*";
120
121    /**
122     * Rights that are skipped for certain target evaluations.
123     * The test is use the skipRights array is:
124     *
125     * Either the ACI has a targetattr's rule and the current
126     * attribute type is null or the current attribute type has
127     * a type specified and the targetattr's rule is null.
128     *
129     * The actual check against the skipRights array is:
130     *
131     *  1. Is the ACI's rights in this array? For example,
132     *     allow(all) or deny(add)
133     *
134     *  AND
135     *
136     *  2. Is the rights from the LDAP operation in this array? For
137     *      example, an LDAP add would have rights of add and all.
138     *
139     *  If both are true, than the target match test returns true
140     *  for this ACI.
141     */
142    private static final int skipRights = ACI_ADD | ACI_DELETE | ACI_PROXY;
143
144    /**
145     * Creates an ACI target from the specified arguments. All of these
146     * may be null. If the ACI has no targets defaults will be used.
147     *
148     * @param targetEntry The ACI target keyword class.
149     * @param targetAttr The ACI targetattr keyword class.
150     * @param targetFilter The ACI targetfilter keyword class.
151     * @param targetScope The ACI targetscope keyword class.
152     * @param targAttrFilters The ACI targAttrFilters keyword class.
153     * @param targetControl The ACI targetControl keyword class.
154     * @param extOp The ACI extop keyword class.
155     */
156    private AciTargets(Target targetEntry, TargetAttr targetAttr,
157                       TargetFilter targetFilter,
158                       SearchScope targetScope,
159                       TargAttrFilters targAttrFilters,
160                       TargetControl targetControl,
161                       ExtOp extOp) {
162       this.target=targetEntry;
163       this.targetAttr=targetAttr;
164       this.targetScope=targetScope;
165       this.targetFilter=targetFilter;
166       this.targAttrFilters=targAttrFilters;
167       this.targetControl=targetControl;
168       this.extOp=extOp;
169    }
170
171    /**
172     * Return class representing the ACI target keyword. May be
173     * null. The default is the use the DN of the entry containing
174     * the ACI and check if the resource entry is a descendant of that.
175     * @return The ACI target class.
176     */
177    private Target getTarget() {
178        return target;
179    }
180
181    /**
182     * Return class representing the ACI targetattr keyword. May be null.
183     * The default is to not match any attribute types in an entry.
184     * @return The ACI targetattr class.
185     */
186    public TargetAttr getTargetAttr() {
187        return targetAttr;
188    }
189
190    /**
191     * Return the ACI targetscope keyword. Default is WHOLE_SUBTREE.
192     * @return The ACI targetscope information.
193     */
194    public SearchScope getTargetScope() {
195        return targetScope;
196    }
197
198    /**
199     * Return class representing the  ACI targetfilter keyword. May be null.
200     * @return The targetscope information.
201     */
202    public TargetFilter getTargetFilter() {
203        return targetFilter;
204    }
205
206    /**
207     * Return the class representing the ACI targattrfilters keyword. May be
208     * null.
209     * @return The targattrfilters information.
210     */
211    public TargAttrFilters getTargAttrFilters() {
212        return targAttrFilters;
213    }
214
215   /**
216    * Return the class representing the ACI targetcontrol keyword. May be
217    * null.
218    * @return The targetcontrol information.
219   */
220    public TargetControl getTargetControl() {
221      return targetControl;
222    }
223
224
225   /**
226    * Return the class representing the ACI extop keyword. May be
227    * null.
228    * @return The extop information.
229   */
230    public ExtOp getExtOp() {
231      return extOp;
232    }
233
234    /**
235     * Decode an ACI's target part of the syntax from the string provided.
236     * @param input String representing an ACI target part of syntax.
237     * @param dn The DN of the entry containing the ACI.
238     * @return An AciTargets class representing the decoded ACI target string.
239     * @throws AciException If the provided string contains errors.
240     */
241    public static AciTargets decode(String input, DN dn)
242    throws AciException {
243        Target target=null;
244        TargetAttr targetAttr=null;
245        TargetFilter targetFilter=null;
246        TargAttrFilters targAttrFilters=null;
247        TargetControl targetControl=null;
248        ExtOp extOp=null;
249        SearchScope targetScope=SearchScope.WHOLE_SUBTREE;
250        Pattern targetPattern = Pattern.compile(targetRegex);
251        Matcher targetMatcher = targetPattern.matcher(input);
252        while (targetMatcher.find())
253        {
254            if (targetMatcher.groupCount() != targetElementCount) {
255                LocalizableMessage message =
256                    WARN_ACI_SYNTAX_INVALID_TARGET_SYNTAX.get(input);
257                throw new AciException(message);
258            }
259            String keyword = targetMatcher.group(targetKeywordPos);
260            EnumTargetKeyword targetKeyword  =
261                EnumTargetKeyword.createKeyword(keyword);
262            if (targetKeyword == null) {
263                LocalizableMessage message =
264                    WARN_ACI_SYNTAX_INVALID_TARGET_KEYWORD.get(keyword);
265                throw new AciException(message);
266            }
267            String operator =
268                targetMatcher.group(targetOperatorPos);
269            EnumTargetOperator targetOperator =
270                EnumTargetOperator.createOperator(operator);
271            if (targetOperator == null) {
272                LocalizableMessage message =
273                    WARN_ACI_SYNTAX_INVALID_TARGETS_OPERATOR.get(operator);
274                throw new AciException(message);
275            }
276            String expression = targetMatcher.group(targetExpressionPos);
277            switch(targetKeyword)
278            {
279            case KEYWORD_TARGET:
280            {
281                if (target == null){
282                    target =  Target.decode(targetOperator, expression, dn);
283                }
284                else
285                {
286                  LocalizableMessage message =
287                          WARN_ACI_SYNTAX_INVALID_TARGET_DUPLICATE_KEYWORDS.
288                                  get("target", input);
289                  throw new AciException(message);
290                }
291                break;
292            }
293            case KEYWORD_TARGETCONTROL:
294            {
295              if (targetControl == null){
296                targetControl =
297                        TargetControl.decode(targetOperator, expression);
298              }
299              else
300              {
301                LocalizableMessage message =
302                        WARN_ACI_SYNTAX_INVALID_TARGET_DUPLICATE_KEYWORDS.
303                                get("targetcontrol", input);
304                throw new AciException(message);
305              }
306              break;
307            }
308            case KEYWORD_EXTOP:
309            {
310              if (extOp == null){
311                extOp =  ExtOp.decode(targetOperator, expression);
312              }
313              else
314              {
315                LocalizableMessage message =
316                        WARN_ACI_SYNTAX_INVALID_TARGET_DUPLICATE_KEYWORDS.
317                                get("extop", input);
318                throw new AciException(message);
319              }
320              break;
321            }
322            case KEYWORD_TARGETATTR:
323            {
324                if (targetAttr == null){
325                    targetAttr = TargetAttr.decode(targetOperator,
326                            expression);
327                }
328                else {
329                  LocalizableMessage message =
330                          WARN_ACI_SYNTAX_INVALID_TARGET_DUPLICATE_KEYWORDS.
331                                  get("targetattr", input);
332                  throw new AciException(message);
333                }
334                break;
335            }
336            case KEYWORD_TARGETSCOPE:
337            {
338                // Check the operator for the targetscope is EQUALITY
339                if (targetOperator == EnumTargetOperator.NOT_EQUALITY) {
340                    LocalizableMessage message =
341                            WARN_ACI_SYNTAX_INVALID_TARGET_NOT_OPERATOR.
342                              get(operator, targetKeyword.name());
343                    throw new AciException(message);
344                }
345                targetScope=createScope(expression);
346                break;
347            }
348            case KEYWORD_TARGETFILTER:
349            {
350                if (targetFilter == null){
351                    targetFilter = TargetFilter.decode(targetOperator,
352                            expression);
353                }
354                else {
355                  LocalizableMessage message =
356                          WARN_ACI_SYNTAX_INVALID_TARGET_DUPLICATE_KEYWORDS.
357                                  get("targetfilter", input);
358                  throw new AciException(message);
359                }
360                break;
361            }
362            case KEYWORD_TARGATTRFILTERS:
363            {
364                if (targAttrFilters == null){
365                    // Check the operator for the targattrfilters is EQUALITY
366                    if (targetOperator == EnumTargetOperator.NOT_EQUALITY) {
367                      LocalizableMessage message =
368                              WARN_ACI_SYNTAX_INVALID_TARGET_NOT_OPERATOR.
369                                      get(operator, targetKeyword.name());
370                      throw new AciException(message);
371                    }
372                    targAttrFilters = TargAttrFilters.decode(targetOperator,
373                            expression);
374                }
375                else {
376                  LocalizableMessage message =
377                          WARN_ACI_SYNTAX_INVALID_TARGET_DUPLICATE_KEYWORDS.
378                                  get("targattrfilters", input);
379                  throw new AciException(message);
380                }
381                break;
382            }
383            }
384        }
385        return new AciTargets(target, targetAttr, targetFilter,
386                              targetScope, targAttrFilters, targetControl,
387                              extOp);
388    }
389
390    /**
391     * Evaluates a provided scope string and returns an appropriate
392     * SearchScope enumeration.
393     * @param expression The expression string.
394     * @return An search scope enumeration matching the string.
395     * @throws AciException If the expression is an invalid targetscope
396     * string.
397     */
398    private static SearchScope createScope(String expression)
399    throws AciException {
400        if(expression.equalsIgnoreCase("base"))
401        {
402          return SearchScope.BASE_OBJECT;
403        }
404        else if(expression.equalsIgnoreCase("onelevel"))
405        {
406          return SearchScope.SINGLE_LEVEL;
407        }
408        else if(expression.equalsIgnoreCase("subtree"))
409        {
410          return SearchScope.WHOLE_SUBTREE;
411        }
412        else if(expression.equalsIgnoreCase("subordinate"))
413        {
414          return SearchScope.SUBORDINATES;
415        }
416        else {
417            LocalizableMessage message =
418                WARN_ACI_SYNTAX_INVALID_TARGETSCOPE_EXPRESSION.get(expression);
419            throw new AciException(message);
420        }
421    }
422
423    /**
424     * Checks an ACI's targetfilter rule information against a target match
425     * context.
426     * @param aci The ACI to try an match the targetfilter of.
427     * @param matchCtx The target match context containing information needed
428     * to perform a target match.
429     * @return True if the targetfilter rule matched the target context.
430     */
431    public static boolean isTargetFilterApplicable(Aci aci,
432                                              AciTargetMatchContext matchCtx) {
433        TargetFilter targetFilter=aci.getTargets().getTargetFilter();
434        return targetFilter == null || targetFilter.isApplicable(matchCtx);
435    }
436
437    /**
438     * Check an ACI's targetcontrol rule against a target match context.
439     *
440     * @param aci The ACI to match the targetcontrol against.
441     * @param matchCtx The target match context containing the information
442     *                 needed to perform the target match.
443     * @return  True if the targetcontrol rule matched the target context.
444     */
445    public static boolean isTargetControlApplicable(Aci aci,
446                                            AciTargetMatchContext matchCtx) {
447      TargetControl targetControl=aci.getTargets().getTargetControl();
448      return targetControl != null && targetControl.isApplicable(matchCtx);
449    }
450
451    /**
452     * Check an ACI's extop rule against a target match context.
453     *
454     * @param aci The ACI to match the extop rule against.
455     * @param matchCtx The target match context containing the information
456     *                 needed to perform the target match.
457     * @return  True if the extop rule matched the target context.
458     */
459    public static boolean isExtOpApplicable(Aci aci,
460                                              AciTargetMatchContext matchCtx) {
461      ExtOp extOp=aci.getTargets().getExtOp();
462      return extOp != null && extOp.isApplicable(matchCtx);
463    }
464
465
466    /**
467     * Check an ACI's targattrfilters rule against a target match context.
468     *
469     * @param aci The ACI to match the targattrfilters against.
470     * @param matchCtx  The target match context containing the information
471     * needed to perform the target match.
472     * @return True if the targattrfilters rule matched the target context.
473     */
474    public static boolean isTargAttrFiltersApplicable(Aci aci,
475                                               AciTargetMatchContext matchCtx) {
476        boolean ret=true;
477        TargAttrFilters targAttrFilters=aci.getTargets().getTargAttrFilters();
478        if(targAttrFilters != null) {
479            if((matchCtx.hasRights(ACI_ADD) &&
480                targAttrFilters.hasMask(TARGATTRFILTERS_ADD)) ||
481              (matchCtx.hasRights(ACI_DELETE) &&
482               targAttrFilters.hasMask(TARGATTRFILTERS_DELETE)))
483            {
484              ret=targAttrFilters.isApplicableAddDel(matchCtx);
485            }
486            else if((matchCtx.hasRights(ACI_WRITE_ADD) &&
487                     targAttrFilters.hasMask(TARGATTRFILTERS_ADD)) ||
488                    (matchCtx.hasRights(ACI_WRITE_DELETE) &&
489                    targAttrFilters.hasMask(TARGATTRFILTERS_DELETE)))
490            {
491              ret=targAttrFilters.isApplicableMod(matchCtx, aci);
492            }
493        }
494        return ret;
495    }
496
497    /*
498     * TODO Evaluate making this method more efficient.
499     * The isTargetAttrApplicable method looks a lot less efficient than it
500     * could be with regard to the logic that it employs and the repeated use
501     * of method calls over local variables.
502     */
503    /**
504     * Checks an provided ACI's targetattr rule against a target match
505     * context.
506     *
507     * @param aci The ACI to evaluate.
508     * @param targetMatchCtx The target match context to check the ACI against.
509     * @return True if the targetattr matched the target context.
510     */
511    public static boolean isTargetAttrApplicable(Aci aci,
512                                         AciTargetMatchContext targetMatchCtx) {
513        boolean ret=true;
514        if(!targetMatchCtx.getTargAttrFiltersMatch()) {
515            TargetAttr targetAttr = aci.getTargets().getTargetAttr();
516            AttributeType attrType = targetMatchCtx.getCurrentAttributeType();
517            boolean isFirstAttr=targetMatchCtx.isFirstAttribute();
518
519            if (attrType != null && targetAttr != null)  {
520              ret=TargetAttr.isApplicable(attrType,targetAttr);
521              setEvalAttributes(targetMatchCtx,targetAttr,ret);
522            } else if (attrType != null || targetAttr != null) {
523                if (aci.hasRights(skipRights)
524                        && skipRightsHasRights(targetMatchCtx.getRights())) {
525                    ret = true;
526                } else {
527                    ret = attrType == null
528                        && targetAttr != null
529                        && aci.hasRights(ACI_WRITE);
530                }
531            }
532            if (isFirstAttr && targetAttr == null
533                && aci.getTargets().getTargAttrFilters() == null)
534            {
535              targetMatchCtx.setEntryTestRule(true);
536            }
537        }
538        return ret;
539    }
540
541    /**
542     * Try and match a one or more of the specified rights in the skiprights
543     * mask.
544     * @param rights The rights to check for.
545     * @return  True if the one or more of the specified rights are in the
546     * skiprights rights mask.
547     */
548    public static boolean skipRightsHasRights(int rights) {
549      //geteffectiverights sets this flag, turn it off before evaluating.
550      int tmpRights=rights & ~ACI_SKIP_PROXY_CHECK;
551      return (skipRights & tmpRights) == tmpRights;
552    }
553
554
555    /**
556     * Wrapper class that passes an ACI, an ACI's targets and the specified
557     * target match context's resource entry DN to the main isTargetApplicable
558     * method.
559     * @param aci The ACI currently be matched.
560     * @param matchCtx The target match context to match against.
561     * @return True if the target matched the ACI.
562     */
563    public static boolean isTargetApplicable(Aci aci,
564                                             AciTargetMatchContext matchCtx) {
565        return isTargetApplicable(aci, aci.getTargets(),
566                                        matchCtx.getResourceEntry().getName());
567    }
568
569    /*
570     * TODO Investigate supporting alternative representations of the scope.
571     *
572     * Should we also consider supporting alternate representations of the
573     * scope values (in particular, allow "one" in addition to "onelevel"
574     * and "sub" in addition to "subtree") to match the very common
575     * abbreviations in widespread use for those terms?
576     */
577    /**
578     * Main target isApplicable method. This method performs the target keyword
579     * match functionality, which allows for directory entry "targeting" using
580     * the specified ACI, ACI targets class and DN.
581     *
582     * @param aci The ACI to match the target against.
583     * @param targets The targets to use in this evaluation.
584     * @param entryDN The DN to use in this evaluation.
585     * @return True if the ACI matched the target and DN.
586     */
587    public static boolean isTargetApplicable(Aci aci,
588            AciTargets targets, DN entryDN) {
589        DN targetDN=aci.getDN();
590        /*
591         * Scoping of the ACI uses either the DN of the entry
592         * containing the ACI (aci.getDN above), or if the ACI item
593         * contains a simple target DN and a equality operator, that
594         * simple target DN is used as the target DN.
595         */
596        if(targets.getTarget() != null && !targets.getTarget().isPattern()) {
597            EnumTargetOperator op=targets.getTarget().getOperator();
598            if(op != EnumTargetOperator.NOT_EQUALITY)
599            {
600              targetDN=targets.getTarget().getDN();
601            }
602        }
603        //Check if the scope is correct.
604        switch(targets.getTargetScope().asEnum()) {
605        case BASE_OBJECT:
606            if(!targetDN.equals(entryDN))
607            {
608              return false;
609            }
610            break;
611        case SINGLE_LEVEL:
612            /*
613             * We use the standard definition of single level to mean the
614             * immediate children only -- not the target entry itself.
615             * Sun CR 6535035 has been raised on DSEE:
616             * Non-standard interpretation of onelevel in ACI targetScope.
617             */
618            if(!targetDN.equals(entryDN.parent()))
619            {
620              return false;
621            }
622            break;
623        case WHOLE_SUBTREE:
624            if(!entryDN.isDescendantOf(targetDN))
625            {
626              return false;
627            }
628            break;
629        case SUBORDINATES:
630            if (entryDN.size() <= targetDN.size() ||
631                 !entryDN.isDescendantOf(targetDN)) {
632              return false;
633            }
634            break;
635        default:
636            return false;
637        }
638        /*
639         * The entry is in scope. For inequality checks, scope was tested
640         * against the entry containing the ACI. If operator is inequality,
641         * check that it doesn't match the target DN.
642         */
643        if(targets.getTarget() != null &&
644                !targets.getTarget().isPattern()) {
645            EnumTargetOperator op=targets.getTarget().getOperator();
646            if(op == EnumTargetOperator.NOT_EQUALITY) {
647                DN tmpDN=targets.getTarget().getDN();
648                if(entryDN.isDescendantOf(tmpDN))
649                {
650                  return false;
651                }
652            }
653        }
654        /*
655         * There is a pattern, need to match the substring filter
656         * created when the ACI was decoded. If inequality flip the
657         * result.
658         */
659        if(targets.getTarget() != null &&
660                targets.getTarget().isPattern())  {
661            final boolean ret = targets.getTarget().matchesPattern(entryDN);
662            EnumTargetOperator op=targets.getTarget().getOperator();
663            if(op == EnumTargetOperator.NOT_EQUALITY)
664            {
665              return !ret;
666            }
667            return ret;
668        }
669        return true;
670    }
671
672
673    /**
674     * The method is used to try and determine if a targetAttr expression that
675     * is applicable has a '*' (or '+' operational attributes) token or if it
676     * was applicable because of a specific attribute type declared in the
677     * targetattrs expression (i.e., targetattrs=cn).
678     *
679     *
680     * @param ctx  The ctx to check against.
681     * @param targetAttr The targetattrs part of the ACI.
682     * @param ret  The is true if the ACI has already been evaluated to be
683     *             applicable.
684     */
685    private static
686    void setEvalAttributes(AciTargetMatchContext ctx, TargetAttr targetAttr,
687                           boolean ret) {
688        ctx.clearEvalAttributes(ACI_USER_ATTR_STAR_MATCHED);
689        ctx.clearEvalAttributes(ACI_OP_ATTR_PLUS_MATCHED);
690        /*
691         If an applicable targetattr's match rule has not
692         been seen (~ACI_FOUND_OP_ATTR_RULE or ~ACI_FOUND_USER_ATTR_RULE) and
693         the current attribute type is applicable because of a targetattr all
694         user (or operational) attributes rule match,
695         set a flag to indicate this situation (ACI_USER_ATTR_STAR_MATCHED or
696         ACI_OP_ATTR_PLUS_MATCHED). This check also catches the following case
697         where the match was by a specific attribute type (either user or
698         operational) and the other attribute type has an all attribute token.
699         For example, the expression is: (targetattrs="cn || +) and the current
700         attribute type is cn.
701        */
702        if(ret && targetAttr.isAllUserAttributes() &&
703                !ctx.hasEvalUserAttributes())
704        {
705          ctx.setEvalUserAttributes(ACI_USER_ATTR_STAR_MATCHED);
706        }
707        else
708        {
709          ctx.setEvalUserAttributes(ACI_FOUND_USER_ATTR_RULE);
710        }
711
712        if(ret && targetAttr.isAllOpAttributes() &&
713                !ctx.hasEvalOpAttributes())
714        {
715          ctx.setEvalOpAttributes(ACI_OP_ATTR_PLUS_MATCHED);
716        }
717        else
718        {
719          ctx.setEvalOpAttributes(ACI_FOUND_OP_ATTR_RULE);
720        }
721    }
722}