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 org.opends.server.types.AttributeType; 034 import org.opends.server.types.DN; 035 import org.opends.server.types.SearchScope; 036 import java.util.regex.Matcher; 037 import java.util.regex.Pattern; 038 039 /** 040 * This class represents target part of an ACI's syntax. This is the part 041 * of an ACI before the ACI body and specifies the entry, attributes, or set 042 * of entries and attributes which the ACI controls access. 043 * 044 * The supported ACI target keywords are: target, targetattr, 045 * targetscope, targetfilter, targattrfilters, targetcontrol and extop. 046 */ 047 public class AciTargets { 048 049 /* 050 * ACI syntax has a target keyword. 051 */ 052 private Target target = null ; 053 054 /* 055 * ACI syntax has a targetscope keyword. 056 */ 057 private SearchScope targetScope = SearchScope.WHOLE_SUBTREE; 058 059 /* 060 * ACI syntax has a targetattr keyword. 061 */ 062 private TargetAttr targetAttr = null ; 063 064 /* 065 * ACI syntax has a targetfilter keyword. 066 */ 067 private TargetFilter targetFilter=null; 068 069 /* 070 * ACI syntax has a targattrtfilters keyword. 071 */ 072 private TargAttrFilters targAttrFilters=null; 073 074 /** 075 * The ACI syntax has a targetcontrol keyword. 076 */ 077 private TargetControl targetControl=null; 078 079 /** 080 * The ACI syntax has a extop keyword. 081 */ 082 private ExtOp extOp=null; 083 084 /* 085 * The number of regular expression group positions in a valid ACI target 086 * expression. 087 */ 088 private static final int targetElementCount = 3; 089 090 /* 091 * Regular expression group position of a target keyword. 092 */ 093 private static final int targetKeywordPos = 1; 094 095 /* 096 * Regular expression group position of a target operator enumeration. 097 */ 098 private static final int targetOperatorPos = 2; 099 100 /* 101 * Regular expression group position of a target expression statement. 102 */ 103 private static final int targetExpressionPos = 3; 104 105 /* 106 * Regular expression used to match a single target rule. 107 */ 108 private static final String targetRegex = 109 OPEN_PAREN + ZERO_OR_MORE_WHITESPACE + WORD_GROUP + 110 ZERO_OR_MORE_WHITESPACE + "(!?=)" + ZERO_OR_MORE_WHITESPACE + 111 "\"([^\"]+)\"" + ZERO_OR_MORE_WHITESPACE + CLOSED_PAREN + 112 ZERO_OR_MORE_WHITESPACE; 113 114 /** 115 * Regular expression used to match one or more target rules. The patern is 116 * part of a general ACI verification. 117 */ 118 public static final String targetsRegex = "(" + targetRegex + ")*"; 119 120 /* 121 * Rights that are skipped for certain target evaluations. 122 * The test is use the skipRights array is: 123 * 124 * Either the ACI has a targetattr's rule and the current 125 * attribute type is null or the current attribute type has 126 * a type specified and the targetattr's rule is null. 127 * 128 * The actual check against the skipRights array is: 129 * 130 * 1. Is the ACI's rights in this array? For example, 131 * allow(all) or deny(add) 132 * 133 * AND 134 * 135 * 2. Is the rights from the LDAP operation in this array? For 136 * example, an LDAP add would have rights of add and all. 137 * 138 * If both are true, than the target match test returns true 139 * for this ACI. 140 */ 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 Message 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 Message 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 Message 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 Message 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 Message 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 Message 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 Message 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 Message 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 Message 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 Message 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 Message 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 return SearchScope.BASE_OBJECT; 402 else if(expression.equalsIgnoreCase("onelevel")) 403 return SearchScope.SINGLE_LEVEL; 404 else if(expression.equalsIgnoreCase("subtree")) 405 return SearchScope.WHOLE_SUBTREE; 406 else if(expression.equalsIgnoreCase("subordinate")) 407 return SearchScope.SUBORDINATE_SUBTREE; 408 else { 409 Message message = 410 WARN_ACI_SYNTAX_INVALID_TARGETSCOPE_EXPRESSION.get(expression); 411 throw new AciException(message); 412 } 413 } 414 415 /** 416 * Checks an ACI's targetfilter rule information against a target match 417 * context. 418 * @param aci The ACI to try an match the targetfilter of. 419 * @param matchCtx The target match context containing information needed 420 * to perform a target match. 421 * @return True if the targetfilter rule matched the target context. 422 */ 423 public static boolean isTargetFilterApplicable(Aci aci, 424 AciTargetMatchContext matchCtx) { 425 boolean ret=true; 426 TargetFilter targetFilter=aci.getTargets().getTargetFilter(); 427 if(targetFilter != null) 428 ret=targetFilter.isApplicable(matchCtx); 429 return ret; 430 } 431 432 /** 433 * Check an ACI's targetcontrol rule against a target match context. 434 * 435 * @param aci The ACI to match the targetcontrol against. 436 * @param matchCtx The target match context containing the information 437 * needed to perform the target match. 438 * @return True if the targetcontrol rule matched the target context. 439 */ 440 public static boolean isTargetControlApplicable(Aci aci, 441 AciTargetMatchContext matchCtx) { 442 boolean ret=false; 443 TargetControl targetControl=aci.getTargets().getTargetControl(); 444 if(targetControl != null) 445 ret=targetControl.isApplicable(matchCtx); 446 return ret; 447 } 448 449 /** 450 * Check an ACI's extop rule against a target match context. 451 * 452 * @param aci The ACI to match the extop rule against. 453 * @param matchCtx The target match context containing the information 454 * needed to perform the target match. 455 * @return True if the extop rule matched the target context. 456 */ 457 public static boolean isExtOpApplicable(Aci aci, 458 AciTargetMatchContext matchCtx) { 459 boolean ret=false; 460 ExtOp extOp=aci.getTargets().getExtOp(); 461 if(extOp != null) 462 ret=extOp.isApplicable(matchCtx); 463 return ret; 464 } 465 466 467 /** 468 * Check an ACI's targattrfilters rule against a target match context. 469 * 470 * @param aci The ACI to match the targattrfilters against. 471 * @param matchCtx The target match context containing the information 472 * needed to perform the target match. 473 * @return True if the targattrfilters rule matched the target context. 474 */ 475 public static boolean isTargAttrFiltersApplicable(Aci aci, 476 AciTargetMatchContext matchCtx) { 477 boolean ret=true; 478 TargAttrFilters targAttrFilters=aci.getTargets().getTargAttrFilters(); 479 if(targAttrFilters != null) { 480 if((matchCtx.hasRights(ACI_ADD) && 481 targAttrFilters.hasMask(TARGATTRFILTERS_ADD)) || 482 (matchCtx.hasRights(ACI_DELETE) && 483 targAttrFilters.hasMask(TARGATTRFILTERS_DELETE))) 484 ret=targAttrFilters.isApplicableAddDel(matchCtx); 485 else if((matchCtx.hasRights(ACI_WRITE_ADD) && 486 targAttrFilters.hasMask(TARGATTRFILTERS_ADD)) || 487 (matchCtx.hasRights(ACI_WRITE_DELETE) && 488 targAttrFilters.hasMask(TARGATTRFILTERS_DELETE))) 489 ret=targAttrFilters.isApplicableMod(matchCtx, aci); 490 } 491 return ret; 492 } 493 494 /* 495 * TODO Evaluate making this method more efficient. 496 * The isTargetAttrApplicable method looks a lot less efficient than it 497 * could be with regard to the logic that it employs and the repeated use 498 * of method calls over local variables. 499 */ 500 /** 501 * Checks an provided ACI's targetattr rule against a target match 502 * context. 503 * 504 * @param aci The ACI to evaluate. 505 * @param targetMatchCtx The target match context to check the ACI against. 506 * @return True if the targetattr matched the target context. 507 */ 508 public static boolean isTargetAttrApplicable(Aci aci, 509 AciTargetMatchContext targetMatchCtx) { 510 boolean ret=true; 511 if(!targetMatchCtx.getTargAttrFiltersMatch()) { 512 AciTargets targets=aci.getTargets(); 513 AttributeType a=targetMatchCtx.getCurrentAttributeType(); 514 int rights=targetMatchCtx.getRights(); 515 boolean isFirstAttr=targetMatchCtx.isFirstAttribute(); 516 if((a != null) && (targets.getTargetAttr() != null)) { 517 ret=TargetAttr.isApplicable(a,targets.getTargetAttr()); 518 setEvalAttributes(targetMatchCtx,targets,ret); 519 } else if((a != null) || (targets.getTargetAttr() != null)) { 520 if((aci.hasRights(skipRights)) && 521 (skipRightsHasRights(rights))) 522 ret=true; 523 else if ((targets.getTargetAttr() != null) && 524 (a == null) && (aci.hasRights(ACI_WRITE))) 525 ret = true; 526 else 527 ret = false; 528 } 529 if((isFirstAttr) && (aci.getTargets().getTargetAttr() == null) 530 && aci.getTargets().getTargAttrFilters() == null) 531 targetMatchCtx.setEntryTestRule(true); 532 } 533 return ret; 534 } 535 536 /** 537 * Try and match a one or more of the specified rights in the skiprights 538 * mask. 539 * @param rights The rights to check for. 540 * @return True if the one or more of the specified rights are in the 541 * skiprights rights mask. 542 */ 543 public static boolean skipRightsHasRights(int rights) { 544 //geteffectiverights sets this flag, turn it off before evaluating. 545 int tmpRights=rights & ~ACI_SKIP_PROXY_CHECK; 546 return ((skipRights & tmpRights) == tmpRights); 547 } 548 549 550 /** 551 * Wrapper class that passes an ACI, an ACI's targets and the specified 552 * target match context's resource entry DN to the main isTargetApplicable 553 * method. 554 * @param aci The ACI currently be matched. 555 * @param matchCtx The target match context to match against. 556 * @return True if the target matched the ACI. 557 */ 558 public static boolean isTargetApplicable(Aci aci, 559 AciTargetMatchContext matchCtx) { 560 return isTargetApplicable(aci, aci.getTargets(), 561 matchCtx.getResourceEntry().getDN()); 562 } 563 564 /* 565 * TODO Investigate supporting alternative representations of the scope. 566 * 567 * Should we also consider supporting alternate representations of the 568 * scope values (in particular, allow "one" in addition to "onelevel" 569 * and "sub" in addition to "subtree") to match the very common 570 * abbreviations in widespread use for those terms? 571 */ 572 /** 573 * Main target isApplicable method. This method performs the target keyword 574 * match functionality, which allows for directory entry "targeting" using 575 * the specifed ACI, ACI targets class and DN. 576 * @param aci The ACI to match the target against. 577 * @param targets The targets to use in this evaluation. 578 * @param entryDN The DN to use in this evaluation. 579 * @return True if the ACI matched the target and DN. 580 */ 581 582 public static boolean isTargetApplicable(Aci aci, 583 AciTargets targets, DN entryDN) { 584 boolean ret=true; 585 DN targetDN=aci.getDN(); 586 /* 587 * Scoping of the ACI uses either the DN of the entry 588 * containing the ACI (aci.getDN above), or if the ACI item 589 * contains a simple target DN and a equality operator, that 590 * simple target DN is used as the target DN. 591 */ 592 if((targets.getTarget() != null) && 593 (!targets.getTarget().isPattern())) { 594 EnumTargetOperator op=targets.getTarget().getOperator(); 595 if(op != EnumTargetOperator.NOT_EQUALITY) 596 targetDN=targets.getTarget().getDN(); 597 } 598 //Check if the scope is correct. 599 switch(targets.getTargetScope()) { 600 case BASE_OBJECT: 601 if(!targetDN.equals(entryDN)) 602 return false; 603 break; 604 case SINGLE_LEVEL: 605 /** 606 * We use the standard definition of single level to mean the 607 * immediate children only -- not the target entry itself. 608 * Sun CR 6535035 has been raised on DSEE: 609 * Non-standard interpretation of onelevel in ACI targetScope. 610 */ 611 if(!entryDN.getParent().equals(targetDN)) 612 return false; 613 break; 614 case WHOLE_SUBTREE: 615 if(!entryDN.isDescendantOf(targetDN)) 616 return false; 617 break; 618 case SUBORDINATE_SUBTREE: 619 if ((entryDN.getNumComponents() <= targetDN.getNumComponents()) || 620 !entryDN.isDescendantOf(targetDN)) { 621 return false; 622 } 623 break; 624 default: 625 return false; 626 } 627 /* 628 * The entry is in scope. For inequality checks, scope was tested 629 * against the entry containing the ACI. If operator is inequality, 630 * check that it doesn't match the target DN. 631 */ 632 if((targets.getTarget() != null) && 633 (!targets.getTarget().isPattern())) { 634 EnumTargetOperator op=targets.getTarget().getOperator(); 635 if(op == EnumTargetOperator.NOT_EQUALITY) { 636 DN tmpDN=targets.getTarget().getDN(); 637 if(entryDN.isDescendantOf(tmpDN)) 638 return false; 639 } 640 } 641 /* 642 * There is a pattern, need to match the substring filter 643 * created when the ACI was decoded. If inequality flip the 644 * result. 645 */ 646 if((targets.getTarget() != null) && 647 (targets.getTarget().isPattern())) { 648 ret=targets.getTarget().matchesPattern(entryDN); 649 EnumTargetOperator op=targets.getTarget().getOperator(); 650 if(op == EnumTargetOperator.NOT_EQUALITY) 651 ret=!ret; 652 } 653 return ret; 654 } 655 656 657 /** 658 * The method is used to try and determine if a targetAttr expression that 659 * is applicable has a '*' (or '+' operational attributes) token or if it 660 * was applicable because of a specific attribute type declared in the 661 * targetattrs expression (i.e., targetattrs=cn). 662 * 663 * 664 * @param ctx The ctx to check against. 665 * @param targets The targets part of the ACI. 666 * @param ret The is true if the ACI has already been evaluated to be 667 * applicable. 668 */ 669 private static 670 void setEvalAttributes(AciTargetMatchContext ctx, AciTargets targets, 671 boolean ret) { 672 ctx.clearEvalAttributes(ACI_USER_ATTR_STAR_MATCHED); 673 ctx.clearEvalAttributes(ACI_OP_ATTR_PLUS_MATCHED); 674 /* 675 If an applicable targetattr's match rule has not 676 been seen (~ACI_FOUND_OP_ATTR_RULE or ~ACI_FOUND_USER_ATTR_RULE) and 677 the current attribute type is applicable because of a targetattr all 678 user (or operational) attributes rule match, 679 set a flag to indicate this situation (ACI_USER_ATTR_STAR_MATCHED or 680 ACI_OP_ATTR_PLUS_MATCHED). This check also catches the following case 681 where the match was by a specific attribute type (either user or 682 operational) and the other attribute type has an all attribute token. 683 For example, the expression is: (targetattrs="cn || +) and the current 684 attribute type is cn. 685 */ 686 if(ret && targets.getTargetAttr().isAllUserAttributes() && 687 !ctx.hasEvalUserAttributes()) 688 ctx.setEvalUserAttributes(ACI_USER_ATTR_STAR_MATCHED); 689 else 690 ctx.setEvalUserAttributes(ACI_FOUND_USER_ATTR_RULE); 691 if(ret && targets.getTargetAttr().isAllOpAttributes() && 692 !ctx.hasEvalOpAttributes()) 693 ctx.setEvalOpAttributes(ACI_OP_ATTR_PLUS_MATCHED); 694 else 695 ctx.setEvalOpAttributes(ACI_FOUND_OP_ATTR_RULE); 696 } 697 }