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