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 2011-2016 ForgeRock AS. 016 */ 017package org.opends.server.workflowelement.localbackend; 018 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.Iterator; 022import java.util.List; 023import java.util.TreeMap; 024 025import org.forgerock.i18n.LocalizableMessage; 026import org.forgerock.i18n.LocalizableMessageBuilder; 027import org.forgerock.i18n.LocalizableMessageDescriptor; 028import org.forgerock.i18n.slf4j.LocalizedLogger; 029import org.forgerock.opendj.ldap.DN; 030import org.forgerock.opendj.ldap.ResultCode; 031import org.forgerock.opendj.ldap.SearchScope; 032import org.opends.server.api.AccessControlHandler; 033import org.opends.server.api.Backend; 034import org.opends.server.backends.RootDSEBackend; 035import org.opends.server.controls.LDAPPostReadRequestControl; 036import org.opends.server.controls.LDAPPostReadResponseControl; 037import org.opends.server.controls.LDAPPreReadRequestControl; 038import org.opends.server.controls.LDAPPreReadResponseControl; 039import org.opends.server.controls.ProxiedAuthV1Control; 040import org.opends.server.controls.ProxiedAuthV2Control; 041import org.opends.server.core.AccessControlConfigManager; 042import org.opends.server.core.AddOperation; 043import org.opends.server.core.BindOperation; 044import org.opends.server.core.CompareOperation; 045import org.opends.server.core.DeleteOperation; 046import org.opends.server.core.DirectoryServer; 047import org.opends.server.core.ModifyDNOperation; 048import org.opends.server.core.ModifyOperation; 049import org.opends.server.core.SearchOperation; 050import org.opends.server.types.AbstractOperation; 051import org.opends.server.types.AdditionalLogItem; 052import org.opends.server.types.CanceledOperationException; 053import org.opends.server.types.Control; 054import org.opends.server.types.DirectoryException; 055import org.opends.server.types.Entry; 056import org.opends.server.types.Operation; 057import org.opends.server.types.OperationType; 058import org.opends.server.types.Privilege; 059import org.opends.server.types.SearchResultEntry; 060import org.opends.server.types.WritabilityMode; 061 062import static org.opends.messages.CoreMessages.*; 063import static org.opends.messages.ProtocolMessages.ERR_PROXYAUTH_AUTHZ_NOT_PERMITTED; 064import static org.opends.server.util.ServerConstants.*; 065 066/** 067 * This class defines a local backend workflow element; e-g an entity that 068 * handle the processing of an operation against a local backend. 069 */ 070public class LocalBackendWorkflowElement 071{ 072 /** 073 * This class implements the workflow result code. The workflow result code 074 * contains an LDAP result code along with an LDAP error message. 075 */ 076 private static class SearchResultCode 077 { 078 /** The global result code. */ 079 private ResultCode resultCode = ResultCode.UNDEFINED; 080 081 /** The global error message. */ 082 private LocalizableMessageBuilder errorMessage = new LocalizableMessageBuilder(LocalizableMessage.EMPTY); 083 084 /** 085 * Creates a new instance of a workflow result code and initializes it with 086 * a result code and an error message. 087 * 088 * @param resultCode 089 * the initial value for the result code 090 * @param errorMessage 091 * the initial value for the error message 092 */ 093 SearchResultCode(ResultCode resultCode, LocalizableMessageBuilder errorMessage) 094 { 095 this.resultCode = resultCode; 096 this.errorMessage = errorMessage; 097 } 098 099 /** 100 * Elaborates a global result code. A workflow may execute an operation on 101 * several subordinate workflows. In such case, the parent workflow has to 102 * take into account all the subordinate result codes to elaborate a global 103 * result code. Sometimes, a referral result code has to be turned into a 104 * reference entry. When such case is occurring the 105 * elaborateGlobalResultCode method will return true. The global result code 106 * is elaborated as follows: 107 * 108 * <PRE> 109 * -----------+------------+------------+------------------------------- 110 * new | current | resulting | 111 * resultCode | resultCode | resultCode | action 112 * -----------+------------+------------+------------------------------- 113 * SUCCESS NO_SUCH_OBJ SUCCESS - 114 * REFERRAL SUCCESS send reference entry to client 115 * other [unchanged] - 116 * --------------------------------------------------------------------- 117 * NO_SUCH_OBJ SUCCESS [unchanged] - 118 * REFERRAL [unchanged] - 119 * other [unchanged] - 120 * --------------------------------------------------------------------- 121 * REFERRAL SUCCESS [unchanged] send reference entry to client 122 * REFERRAL SUCCESS send reference entry to client 123 * NO_SUCH_OBJ REFERRAL - 124 * other [unchanged] send reference entry to client 125 * --------------------------------------------------------------------- 126 * others SUCCESS other - 127 * REFERRAL other send reference entry to client 128 * NO_SUCH_OBJ other - 129 * other2 [unchanged] - 130 * --------------------------------------------------------------------- 131 * </PRE> 132 * 133 * @param newResultCode 134 * the new result code to take into account 135 * @param newErrorMessage 136 * the new error message associated to the new error code 137 * @return <code>true</code> if a referral result code must be turned into a 138 * reference entry 139 */ 140 private boolean elaborateGlobalResultCode(ResultCode newResultCode, LocalizableMessageBuilder newErrorMessage) 141 { 142 // if global result code has not been set yet then just take the new 143 // result code as is 144 if (resultCode == ResultCode.UNDEFINED) 145 { 146 resultCode = newResultCode; 147 errorMessage = new LocalizableMessageBuilder(newErrorMessage); 148 return false; 149 } 150 151 // Elaborate the new result code (see table in the description header). 152 switch (newResultCode.asEnum()) 153 { 154 case SUCCESS: 155 switch (resultCode.asEnum()) 156 { 157 case NO_SUCH_OBJECT: 158 resultCode = ResultCode.SUCCESS; 159 errorMessage = new LocalizableMessageBuilder(LocalizableMessage.EMPTY); 160 return false; 161 case REFERRAL: 162 resultCode = ResultCode.SUCCESS; 163 errorMessage = new LocalizableMessageBuilder(LocalizableMessage.EMPTY); 164 return true; 165 default: 166 // global resultCode remains the same 167 return false; 168 } 169 170 case NO_SUCH_OBJECT: 171 // global resultCode remains the same 172 return false; 173 174 case REFERRAL: 175 switch (resultCode.asEnum()) 176 { 177 case REFERRAL: 178 resultCode = ResultCode.SUCCESS; 179 errorMessage = new LocalizableMessageBuilder(LocalizableMessage.EMPTY); 180 return true; 181 case NO_SUCH_OBJECT: 182 resultCode = ResultCode.REFERRAL; 183 errorMessage = new LocalizableMessageBuilder(LocalizableMessage.EMPTY); 184 return false; 185 default: 186 // global resultCode remains the same 187 return true; 188 } 189 190 default: 191 switch (resultCode.asEnum()) 192 { 193 case REFERRAL: 194 resultCode = newResultCode; 195 errorMessage = new LocalizableMessageBuilder(newErrorMessage); 196 return true; 197 case SUCCESS: 198 case NO_SUCH_OBJECT: 199 resultCode = newResultCode; 200 errorMessage = new LocalizableMessageBuilder(newErrorMessage); 201 return false; 202 default: 203 // Do nothing (we don't want to override the first error) 204 return false; 205 } 206 } 207 } 208 } 209 210 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 211 212 /** The backend's baseDN mapped by this object. */ 213 private final DN baseDN; 214 215 /** The backend associated with the local workflow element. */ 216 private final Backend<?> backend; 217 218 /** The set of local backend workflow elements registered with the server. */ 219 private static TreeMap<DN, LocalBackendWorkflowElement> registeredLocalBackends = new TreeMap<>(); 220 221 /** A lock to guarantee safe concurrent access to the registeredLocalBackends variable. */ 222 private static final Object registeredLocalBackendsLock = new Object(); 223 224 /** 225 * Creates a new instance of the local backend workflow element. 226 * 227 * @param baseDN 228 * the backend's baseDN mapped by this object 229 * @param backend 230 * the backend associated to that workflow element 231 */ 232 private LocalBackendWorkflowElement(DN baseDN, Backend<?> backend) 233 { 234 this.baseDN = baseDN; 235 this.backend = backend; 236 } 237 238 /** 239 * Indicates whether the workflow element encapsulates a private local backend. 240 * 241 * @return <code>true</code> if the workflow element encapsulates a private 242 * local backend, <code>false</code> otherwise 243 */ 244 public boolean isPrivate() 245 { 246 return this.backend != null && this.backend.isPrivateBackend(); 247 } 248 249 /** 250 * Creates and registers a local backend with the server. 251 * 252 * @param baseDN 253 * the backend's baseDN mapped by this object 254 * @param backend 255 * the backend to associate with the local backend workflow element 256 * @return the existing local backend workflow element if it was already 257 * created or a newly created local backend workflow element. 258 */ 259 public static LocalBackendWorkflowElement createAndRegister(DN baseDN, Backend<?> backend) 260 { 261 LocalBackendWorkflowElement localBackend = registeredLocalBackends.get(baseDN); 262 if (localBackend == null) 263 { 264 localBackend = new LocalBackendWorkflowElement(baseDN, backend); 265 registerLocalBackend(localBackend); 266 } 267 return localBackend; 268 } 269 270 /** 271 * Removes a local backend that was registered with the server. 272 * 273 * @param baseDN 274 * the identifier of the workflow to remove 275 */ 276 public static void remove(DN baseDN) 277 { 278 deregisterLocalBackend(baseDN); 279 } 280 281 /** 282 * Removes all the local backends that were registered with the server. 283 * This function is intended to be called when the server is shutting down. 284 */ 285 public static void removeAll() 286 { 287 synchronized (registeredLocalBackendsLock) 288 { 289 for (LocalBackendWorkflowElement localBackend : registeredLocalBackends.values()) 290 { 291 deregisterLocalBackend(localBackend.getBaseDN()); 292 } 293 } 294 } 295 296 /** 297 * Check if an OID is for a proxy authorization control. 298 * 299 * @param oid The OID to check 300 * @return <code>true</code> if the OID is for a proxy auth v1 or v2 control, 301 * <code>false</code> otherwise. 302 */ 303 static boolean isProxyAuthzControl(String oid) 304 { 305 return OID_PROXIED_AUTH_V1.equals(oid) || OID_PROXIED_AUTH_V2.equals(oid); 306 } 307 308 /** 309 * Removes all the disallowed request controls from the provided operation. 310 * <p> 311 * As per RFC 4511 4.1.11, if a disallowed request control is critical, then a 312 * DirectoryException is thrown with unavailableCriticalExtension. Otherwise, 313 * if the disallowed request control is non critical, it is removed because we 314 * do not want the backend to process it. 315 * 316 * @param operation 317 * the operation currently processed 318 * @throws DirectoryException 319 * If a disallowed request control is critical, thrown with 320 * unavailableCriticalExtension. If an error occurred while 321 * performing the access control check. For example, if an attribute 322 * could not be decoded. Care must be taken not to expose any 323 * potentially sensitive information in the exception. 324 */ 325 static void removeAllDisallowedControls(DN targetDN, Operation operation) throws DirectoryException 326 { 327 for (Iterator<Control> iter = operation.getRequestControls().iterator(); iter.hasNext();) 328 { 329 final Control control = iter.next(); 330 if (isProxyAuthzControl(control.getOID())) 331 { 332 continue; 333 } 334 335 if (!getAccessControlHandler().isAllowed(targetDN, operation, control)) 336 { 337 // As per RFC 4511 4.1.11. 338 if (control.isCritical()) 339 { 340 throw new DirectoryException(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION, 341 ERR_CONTROL_INSUFFICIENT_ACCESS_RIGHTS.get(control.getOID())); 342 } 343 344 // We do not want the backend to process this non-critical control, so remove it. 345 iter.remove(); 346 } 347 } 348 } 349 350 /** 351 * Evaluate all aci and privilege checks for any proxy auth controls. 352 * This must be done before evaluating all other controls so that their aci 353 * can then be checked correctly. 354 * 355 * @param operation The operation containing the controls 356 * @throws DirectoryException if a proxy auth control is found but cannot 357 * be used. 358 */ 359 static void evaluateProxyAuthControls(Operation operation) throws DirectoryException 360 { 361 for (Control control : operation.getRequestControls()) 362 { 363 final String oid = control.getOID(); 364 if (isProxyAuthzControl(oid)) 365 { 366 DN authDN = operation.getClientConnection().getAuthenticationInfo().getAuthenticationDN(); 367 if (getAccessControlHandler().isAllowed(authDN, operation, control)) 368 { 369 processProxyAuthControls(operation, oid); 370 } 371 else 372 { 373 // As per RFC 4511 4.1.11. 374 if (control.isCritical()) 375 { 376 throw new DirectoryException(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION, 377 ERR_CONTROL_INSUFFICIENT_ACCESS_RIGHTS.get(control.getOID())); 378 } 379 } 380 } 381 } 382 } 383 384 /** 385 * Check the requester has the PROXIED_AUTH privilege in order to be able to use a proxy auth control. 386 * 387 * @param operation The operation being checked 388 * @throws DirectoryException If insufficient privileges are detected 389 */ 390 private static void checkPrivilegeForProxyAuthControl(Operation operation) throws DirectoryException 391 { 392 if (! operation.getClientConnection().hasPrivilege(Privilege.PROXIED_AUTH, operation)) 393 { 394 throw new DirectoryException(ResultCode.AUTHORIZATION_DENIED, 395 ERR_PROXYAUTH_INSUFFICIENT_PRIVILEGES.get()); 396 } 397 } 398 399 /** 400 * Check the requester has the authorization user in scope of proxy aci. 401 * 402 * @param operation The operation being checked 403 * @param authorizationEntry The entry being authorized as (e.g. from a proxy auth control) 404 * @throws DirectoryException If no proxy permission is allowed 405 */ 406 private static void checkAciForProxyAuthControl(Operation operation, Entry authorizationEntry) 407 throws DirectoryException 408 { 409 if (! AccessControlConfigManager.getInstance().getAccessControlHandler() 410 .mayProxy(operation.getClientConnection().getAuthenticationInfo().getAuthenticationEntry(), 411 authorizationEntry, operation)) 412 { 413 throw new DirectoryException(ResultCode.AUTHORIZATION_DENIED, 414 ERR_PROXYAUTH_AUTHZ_NOT_PERMITTED.get(authorizationEntry.getName())); 415 } 416 } 417 /** 418 * Process the operation control with the given oid if it is a proxy auth control. 419 * 420 * Privilege and initial aci checks on the authenticating user are performed. The authenticating 421 * user must have the proxied-auth privilege, and the authz user must be in the scope of aci 422 * allowing the proxy right to the authenticating user. 423 * 424 * @param operation The operation containing the control(s) 425 * @param oid The OID of the detected proxy auth control 426 * @throws DirectoryException 427 */ 428 private static void processProxyAuthControls(Operation operation, String oid) 429 throws DirectoryException 430 { 431 final Entry authorizationEntry; 432 433 if (OID_PROXIED_AUTH_V1.equals(oid)) 434 { 435 final ProxiedAuthV1Control proxyControlV1 = operation.getRequestControl(ProxiedAuthV1Control.DECODER); 436 // Log usage of legacy proxy authz V1 control. 437 operation.addAdditionalLogItem(AdditionalLogItem.keyOnly(operation.getClass(), 438 "obsoleteProxiedAuthzV1Control")); 439 checkPrivilegeForProxyAuthControl(operation); 440 authorizationEntry = proxyControlV1.getAuthorizationEntry(); 441 } 442 else if (OID_PROXIED_AUTH_V2.equals(oid)) 443 { 444 final ProxiedAuthV2Control proxyControlV2 = operation.getRequestControl(ProxiedAuthV2Control.DECODER); 445 checkPrivilegeForProxyAuthControl(operation); 446 authorizationEntry = proxyControlV2.getAuthorizationEntry(); 447 } 448 else 449 { 450 return; 451 } 452 453 checkAciForProxyAuthControl(operation, authorizationEntry); 454 operation.setAuthorizationEntry(authorizationEntry); 455 456 operation.setProxiedAuthorizationDN( 457 authorizationEntry != null ? authorizationEntry.getName() : DN.rootDN()); 458 } 459 460 /** 461 * Returns a new {@link DirectoryException} built from the provided 462 * resultCodes and messages. Depending on whether ACIs prevent information 463 * disclosure, the provided resultCode and message will be masked and 464 * altResultCode and altMessage will be used instead. 465 * 466 * @param operation 467 * the operation for which to check if ACIs prevent information 468 * disclosure 469 * @param entry 470 * the entry for which to check if ACIs prevent information 471 * disclosure, if null, then a fake entry will be created from the 472 * entryDN parameter 473 * @param entryDN 474 * the entry dn for which to check if ACIs prevent information 475 * disclosure. Only used if entry is null. 476 * @param resultCode 477 * the result code to put on the DirectoryException if ACIs allow 478 * disclosure. Otherwise it will be put on the DirectoryException as 479 * a masked result code. 480 * @param message 481 * the message to put on the DirectoryException if ACIs allow 482 * disclosure. Otherwise it will be put on the DirectoryException as 483 * a masked message. 484 * @param altResultCode 485 * the result code to put on the DirectoryException if ACIs do not 486 * allow disclosing the resultCode. 487 * @param altMessage 488 * the result code to put on the DirectoryException if ACIs do not 489 * allow disclosing the message. 490 * @return a new DirectoryException containing the provided resultCodes and 491 * messages depending on ACI allowing disclosure or not 492 * @throws DirectoryException 493 * If an error occurred while performing the access control check. 494 */ 495 static DirectoryException newDirectoryException(Operation operation, 496 Entry entry, DN entryDN, ResultCode resultCode, LocalizableMessage message, 497 ResultCode altResultCode, LocalizableMessage altMessage) throws DirectoryException 498 { 499 if (getAccessControlHandler().canDiscloseInformation(entry, entryDN, operation)) 500 { 501 return new DirectoryException(resultCode, message); 502 } 503 // replacement reason returned to the user 504 final DirectoryException ex = new DirectoryException(altResultCode, altMessage); 505 // real underlying reason 506 ex.setMaskedResultCode(resultCode); 507 ex.setMaskedMessage(message); 508 return ex; 509 } 510 511 /** 512 * Sets the provided resultCodes and messages on the provided operation. 513 * Depending on whether ACIs prevent information disclosure, the provided 514 * resultCode and message will be masked and altResultCode and altMessage will 515 * be used instead. 516 * 517 * @param operation 518 * the operation for which to check if ACIs prevent information 519 * disclosure 520 * @param entry 521 * the entry for which to check if ACIs prevent information 522 * disclosure, if null, then a fake entry will be created from the 523 * entryDN parameter 524 * @param entryDN 525 * the entry dn for which to check if ACIs prevent information 526 * disclosure. Only used if entry is null. 527 * @param resultCode 528 * the result code to put on the DirectoryException if ACIs allow 529 * disclosure. Otherwise it will be put on the DirectoryException as 530 * a masked result code. 531 * @param message 532 * the message to put on the DirectoryException if ACIs allow 533 * disclosure. Otherwise it will be put on the DirectoryException as 534 * a masked message. 535 * @param altResultCode 536 * the result code to put on the DirectoryException if ACIs do not 537 * allow disclosing the resultCode. 538 * @param altMessage 539 * the result code to put on the DirectoryException if ACIs do not 540 * allow disclosing the message. 541 * @throws DirectoryException 542 * If an error occurred while performing the access control check. 543 */ 544 static void setResultCodeAndMessageNoInfoDisclosure(Operation operation, 545 Entry entry, DN entryDN, ResultCode resultCode, LocalizableMessage message, 546 ResultCode altResultCode, LocalizableMessage altMessage) throws DirectoryException 547 { 548 if (getAccessControlHandler().canDiscloseInformation(entry, entryDN, operation)) 549 { 550 operation.setResultCode(resultCode); 551 operation.appendErrorMessage(message); 552 } 553 else 554 { 555 // replacement reason returned to the user 556 operation.setResultCode(altResultCode); 557 operation.appendErrorMessage(altMessage); 558 // real underlying reason 559 operation.setMaskedResultCode(resultCode); 560 operation.appendMaskedErrorMessage(message); 561 } 562 } 563 564 /** 565 * Removes the matchedDN from the supplied operation if ACIs prevent its 566 * disclosure. 567 * 568 * @param operation 569 * where to filter the matchedDN from 570 */ 571 static void filterNonDisclosableMatchedDN(Operation operation) 572 { 573 if (operation.getMatchedDN() == null) 574 { 575 return; 576 } 577 578 try 579 { 580 if (!getAccessControlHandler().canDiscloseInformation(null, operation.getMatchedDN(), operation)) 581 { 582 operation.setMatchedDN(null); 583 } 584 } 585 catch (DirectoryException de) 586 { 587 logger.traceException(de); 588 589 operation.setResponseData(de); 590 // At this point it is impossible to tell whether the matchedDN can be 591 // disclosed. It is probably safer to hide it by default. 592 operation.setMatchedDN(null); 593 } 594 } 595 596 /** 597 * Adds the post-read response control to the response if requested. 598 * 599 * @param operation 600 * The update operation. 601 * @param postReadRequest 602 * The request control, if present. 603 * @param entry 604 * The post-update entry. 605 */ 606 static void addPostReadResponse(final Operation operation, 607 final LDAPPostReadRequestControl postReadRequest, final Entry entry) 608 { 609 if (postReadRequest == null) 610 { 611 return; 612 } 613 614 /* 615 * Virtual and collective attributes are only added to an entry when it is 616 * read from the backend, not before it is written, so we need to add them 617 * ourself. 618 */ 619 final Entry fullEntry = entry.duplicate(true); 620 621 // Even though the associated update succeeded, 622 // we should still check whether or not we should return the entry. 623 final SearchResultEntry unfilteredSearchEntry = new SearchResultEntry(fullEntry, null); 624 if (getAccessControlHandler().maySend(operation, unfilteredSearchEntry)) 625 { 626 // Filter the entry based on the control's attribute list. 627 final Entry filteredEntry = fullEntry.filterEntry(postReadRequest.getRequestedAttributes(), false, false, false); 628 final SearchResultEntry filteredSearchEntry = new SearchResultEntry(filteredEntry, null); 629 630 // Strip out any attributes which access control denies access to. 631 getAccessControlHandler().filterEntry(operation, unfilteredSearchEntry, filteredSearchEntry); 632 633 operation.addResponseControl(new LDAPPostReadResponseControl(filteredSearchEntry)); 634 } 635 } 636 637 /** 638 * Adds the pre-read response control to the response if requested. 639 * 640 * @param operation 641 * The update operation. 642 * @param preReadRequest 643 * The request control, if present. 644 * @param entry 645 * The pre-update entry. 646 */ 647 static void addPreReadResponse(final Operation operation, 648 final LDAPPreReadRequestControl preReadRequest, final Entry entry) 649 { 650 if (preReadRequest == null) 651 { 652 return; 653 } 654 655 // Even though the associated update succeeded, 656 // we should still check whether or not we should return the entry. 657 final SearchResultEntry unfilteredSearchEntry = new SearchResultEntry(entry, null); 658 if (getAccessControlHandler().maySend(operation, unfilteredSearchEntry)) 659 { 660 // Filter the entry based on the control's attribute list. 661 final Entry filteredEntry = entry.filterEntry(preReadRequest.getRequestedAttributes(), false, false, false); 662 final SearchResultEntry filteredSearchEntry = new SearchResultEntry(filteredEntry, null); 663 664 // Strip out any attributes which access control denies access to. 665 getAccessControlHandler().filterEntry(operation, unfilteredSearchEntry, filteredSearchEntry); 666 667 operation.addResponseControl(new LDAPPreReadResponseControl(filteredSearchEntry)); 668 } 669 } 670 671 private static AccessControlHandler<?> getAccessControlHandler() 672 { 673 return AccessControlConfigManager.getInstance().getAccessControlHandler(); 674 } 675 676 /** 677 * Registers a local backend with the server. 678 * 679 * @param localBackend the local backend to register with the server 680 */ 681 private static void registerLocalBackend(LocalBackendWorkflowElement localBackend) 682 { 683 synchronized (registeredLocalBackendsLock) 684 { 685 DN baseDN = localBackend.getBaseDN(); 686 LocalBackendWorkflowElement existingLocalBackend = registeredLocalBackends.get(baseDN); 687 if (existingLocalBackend == null) 688 { 689 TreeMap<DN, LocalBackendWorkflowElement> newLocalBackends = new TreeMap<>(registeredLocalBackends); 690 newLocalBackends.put(baseDN, localBackend); 691 registeredLocalBackends = newLocalBackends; 692 } 693 } 694 } 695 696 /** 697 * Deregisters a local backend with the server. 698 * 699 * @param baseDN 700 * the identifier of the local backend to remove 701 */ 702 private static void deregisterLocalBackend(DN baseDN) 703 { 704 synchronized (registeredLocalBackendsLock) 705 { 706 LocalBackendWorkflowElement existingLocalBackend = registeredLocalBackends.get(baseDN); 707 if (existingLocalBackend != null) 708 { 709 TreeMap<DN, LocalBackendWorkflowElement> newLocalBackends = new TreeMap<>(registeredLocalBackends); 710 newLocalBackends.remove(baseDN); 711 registeredLocalBackends = newLocalBackends; 712 } 713 } 714 } 715 716 /** 717 * Executes the workflow for an operation. 718 * 719 * @param operation 720 * the operation to execute 721 * @throws CanceledOperationException 722 * if this operation should be canceled 723 */ 724 private void execute(Operation operation) throws CanceledOperationException { 725 switch (operation.getOperationType()) 726 { 727 case BIND: 728 new LocalBackendBindOperation((BindOperation) operation).processLocalBind(this); 729 break; 730 731 case SEARCH: 732 new LocalBackendSearchOperation((SearchOperation) operation).processLocalSearch(this); 733 break; 734 735 case ADD: 736 new LocalBackendAddOperation((AddOperation) operation).processLocalAdd(this); 737 break; 738 739 case DELETE: 740 new LocalBackendDeleteOperation((DeleteOperation) operation).processLocalDelete(this); 741 break; 742 743 case MODIFY: 744 new LocalBackendModifyOperation((ModifyOperation) operation).processLocalModify(this); 745 break; 746 747 case MODIFY_DN: 748 new LocalBackendModifyDNOperation((ModifyDNOperation) operation).processLocalModifyDN(this); 749 break; 750 751 case COMPARE: 752 new LocalBackendCompareOperation((CompareOperation) operation).processLocalCompare(this); 753 break; 754 755 case ABANDON: 756 // There is no processing for an abandon operation. 757 break; 758 759 default: 760 throw new AssertionError("Attempted to execute an invalid operation type: " 761 + operation.getOperationType() + " (" + operation + ")"); 762 } 763 } 764 765 /** 766 * Attaches the current local operation to the global operation so that 767 * operation runner can execute local operation post response later on. 768 * 769 * @param <O> subtype of Operation 770 * @param <L> subtype of LocalBackendOperation 771 * @param globalOperation the global operation to which local operation 772 * should be attached to 773 * @param currentLocalOperation the local operation to attach to the global 774 * operation 775 */ 776 @SuppressWarnings("unchecked") 777 static <O extends Operation, L> void attachLocalOperation(O globalOperation, L currentLocalOperation) 778 { 779 List<?> existingAttachment = (List<?>) globalOperation.getAttachment(Operation.LOCALBACKENDOPERATIONS); 780 List<L> newAttachment = new ArrayList<>(); 781 782 if (existingAttachment != null) 783 { 784 // This line raises an unchecked conversion warning. 785 // There is nothing we can do to prevent this warning 786 // so let's get rid of it since we know the cast is safe. 787 newAttachment.addAll ((List<L>) existingAttachment); 788 } 789 newAttachment.add (currentLocalOperation); 790 globalOperation.setAttachment(Operation.LOCALBACKENDOPERATIONS, newAttachment); 791 } 792 793 /** 794 * Provides the workflow element identifier. 795 * 796 * @return the workflow element identifier 797 */ 798 public DN getBaseDN() 799 { 800 return baseDN; 801 } 802 803 /** 804 * Gets the backend associated with this local backend workflow 805 * element. 806 * 807 * @return The backend associated with this local backend workflow 808 * element. 809 */ 810 public Backend<?> getBackend() 811 { 812 return backend; 813 } 814 815 /** 816 * Checks if an update operation can be performed against a backend. The 817 * operation will be rejected based on the server and backend writability 818 * modes. 819 * 820 * @param backend 821 * The backend handling the update. 822 * @param op 823 * The update operation. 824 * @param entryDN 825 * The name of the entry being updated. 826 * @param serverMsg 827 * The message to log if the update was rejected because the server 828 * is read-only. 829 * @param backendMsg 830 * The message to log if the update was rejected because the backend 831 * is read-only. 832 * @throws DirectoryException 833 * If the update operation has been rejected. 834 */ 835 static void checkIfBackendIsWritable(Backend<?> backend, Operation op, 836 DN entryDN, LocalizableMessageDescriptor.Arg1<Object> serverMsg, 837 LocalizableMessageDescriptor.Arg1<Object> backendMsg) 838 throws DirectoryException 839 { 840 if (!backend.isPrivateBackend()) 841 { 842 checkIfWritable(DirectoryServer.getWritabilityMode(), op, serverMsg, entryDN); 843 checkIfWritable(backend.getWritabilityMode(), op, backendMsg, entryDN); 844 } 845 } 846 847 private static void checkIfWritable(WritabilityMode writabilityMode, Operation op, 848 LocalizableMessageDescriptor.Arg1<Object> errorMsg, DN entryDN) throws DirectoryException 849 { 850 switch (writabilityMode) 851 { 852 case DISABLED: 853 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, errorMsg.get(entryDN)); 854 855 case INTERNAL_ONLY: 856 if (!op.isInternalOperation() && !op.isSynchronizationOperation()) 857 { 858 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, errorMsg.get(entryDN)); 859 } 860 } 861 } 862 863 /** 864 * Executes the supplied operation. 865 * 866 * @param operation 867 * the operation to execute 868 * @param entryDN 869 * the entry DN whose backend will be used 870 * @return true if the operation successfully executed, false otherwise 871 * @throws CanceledOperationException 872 * if this operation should be cancelled. 873 */ 874 public static boolean execute(Operation operation, DN entryDN) throws CanceledOperationException 875 { 876 LocalBackendWorkflowElement workflow = getLocalBackendWorkflowElement(entryDN); 877 if (workflow == null) 878 { 879 // We have found no backend for the requested base DN, 880 // just return a no such entry result code and stop the processing. 881 if (operation instanceof AbstractOperation) 882 { 883 ((AbstractOperation) operation).updateOperationErrMsgAndResCode(); 884 } 885 return false; 886 } 887 888 if (workflow.getBaseDN().isRootDN()) 889 { 890 executeOnRootDSE(operation, workflow); 891 } 892 else 893 { 894 executeOnNonRootDSE(operation, workflow); 895 } 896 return true; 897 } 898 899 private static LocalBackendWorkflowElement getLocalBackendWorkflowElement(DN entryDN) 900 { 901 while (entryDN != null) 902 { 903 final LocalBackendWorkflowElement workflow = registeredLocalBackends.get(entryDN); 904 if (workflow != null) 905 { 906 return workflow; 907 } 908 entryDN = DirectoryServer.getParentDNInSuffix(entryDN); 909 } 910 return null; 911 } 912 913 /** 914 * Executes an operation on the root DSE entry. 915 * 916 * @param operation 917 * the operation to execute 918 * @param workflow 919 * the workflow where to execute the operation 920 * @throws CanceledOperationException 921 * if this operation should be cancelled. 922 */ 923 private static void executeOnRootDSE(Operation operation, LocalBackendWorkflowElement workflow) 924 throws CanceledOperationException 925 { 926 OperationType operationType = operation.getOperationType(); 927 if (operationType == OperationType.SEARCH) 928 { 929 executeSearch((SearchOperation) operation, workflow); 930 } 931 else 932 { 933 workflow.execute(operation); 934 } 935 } 936 937 /** 938 * Executes a search operation on the the root DSE entry. 939 * 940 * @param searchOp 941 * the operation to execute 942 * @param workflow 943 * the workflow where to execute the operation 944 * @throws CanceledOperationException 945 * if this operation should be cancelled. 946 */ 947 private static void executeSearch(SearchOperation searchOp, LocalBackendWorkflowElement workflow) 948 throws CanceledOperationException 949 { 950 // Keep a the original search scope because we will alter it in the operation 951 SearchScope originalScope = searchOp.getScope(); 952 953 // Search base? 954 // The root DSE entry itself is never returned unless the operation 955 // is a search base on the null suffix. 956 if (originalScope == SearchScope.BASE_OBJECT) 957 { 958 workflow.execute(searchOp); 959 return; 960 } 961 962 // Create a workflow result code in case we need to perform search in 963 // subordinate workflows. 964 SearchResultCode searchResultCode = 965 new SearchResultCode(searchOp.getResultCode(), searchOp.getErrorMessage()); 966 967 // The search scope is not 'base', so let's do a search on all the public 968 // naming contexts with appropriate new search scope and new base DN. 969 SearchScope newScope = elaborateScopeForSearchInSubordinates(originalScope); 970 searchOp.setScope(newScope); 971 DN originalBaseDN = searchOp.getBaseDN(); 972 973 for (LocalBackendWorkflowElement subordinate : getRootDSESubordinates()) 974 { 975 // We have to change the operation request base DN to match the 976 // subordinate workflow base DN. Otherwise the workflow will 977 // return a no such entry result code as the operation request 978 // base DN is a superior of the workflow base DN! 979 DN ncDN = subordinate.getBaseDN(); 980 981 // Set the new request base DN then do execute the operation 982 // in the naming context workflow. 983 searchOp.setBaseDN(ncDN); 984 execute(searchOp, ncDN); 985 boolean sendReferenceEntry = searchResultCode.elaborateGlobalResultCode( 986 searchOp.getResultCode(), searchOp.getErrorMessage()); 987 if (sendReferenceEntry) 988 { 989 // TODO jdemendi - turn a referral result code into a reference entry 990 // and send the reference entry to the client application 991 } 992 } 993 994 // Now restore the original request base DN and original search scope 995 searchOp.setBaseDN(originalBaseDN); 996 searchOp.setScope(originalScope); 997 998 // If the result code is still uninitialized (ie no naming context), 999 // we should return NO_SUCH_OBJECT 1000 searchResultCode.elaborateGlobalResultCode( 1001 ResultCode.NO_SUCH_OBJECT, new LocalizableMessageBuilder(LocalizableMessage.EMPTY)); 1002 1003 // Set the operation result code and error message 1004 searchOp.setResultCode(searchResultCode.resultCode); 1005 searchOp.setErrorMessage(searchResultCode.errorMessage); 1006 } 1007 1008 private static Collection<LocalBackendWorkflowElement> getRootDSESubordinates() 1009 { 1010 final RootDSEBackend rootDSEBackend = DirectoryServer.getRootDSEBackend(); 1011 1012 final List<LocalBackendWorkflowElement> results = new ArrayList<>(); 1013 for (DN subordinateBaseDN : rootDSEBackend.getSubordinateBaseDNs().keySet()) 1014 { 1015 results.add(registeredLocalBackends.get(subordinateBaseDN)); 1016 } 1017 return results; 1018 } 1019 1020 private static void executeOnNonRootDSE(Operation operation, LocalBackendWorkflowElement workflow) 1021 throws CanceledOperationException 1022 { 1023 workflow.execute(operation); 1024 1025 // For subtree search operation we need to go through the subordinate nodes. 1026 if (operation.getOperationType() == OperationType.SEARCH) 1027 { 1028 executeSearchOnSubordinates((SearchOperation) operation, workflow); 1029 } 1030 } 1031 1032 /** 1033 * Executes a search operation on the subordinate workflows. 1034 * 1035 * @param searchOp 1036 * the search operation to execute 1037 * @param workflow 1038 * the workflow element 1039 * @throws CanceledOperationException 1040 * if this operation should be canceled. 1041 */ 1042 private static void executeSearchOnSubordinates(SearchOperation searchOp, LocalBackendWorkflowElement workflow) 1043 throws CanceledOperationException { 1044 // If the scope of the search is 'base' then it's useless to search 1045 // in the subordinate workflows. 1046 SearchScope originalScope = searchOp.getScope(); 1047 if (originalScope == SearchScope.BASE_OBJECT) 1048 { 1049 return; 1050 } 1051 1052 // Elaborate the new search scope before executing the search operation 1053 // in the subordinate workflows. 1054 SearchScope newScope = elaborateScopeForSearchInSubordinates(originalScope); 1055 searchOp.setScope(newScope); 1056 1057 // Let's search in the subordinate workflows. 1058 SearchResultCode searchResultCode = new SearchResultCode(searchOp.getResultCode(), searchOp.getErrorMessage()); 1059 DN originalBaseDN = searchOp.getBaseDN(); 1060 for (LocalBackendWorkflowElement subordinate : getSubordinates(workflow)) 1061 { 1062 // We have to change the operation request base DN to match the 1063 // subordinate workflow base DN. Otherwise the workflow will 1064 // return a no such entry result code as the operation request 1065 // base DN is a superior of the subordinate workflow base DN. 1066 DN subordinateDN = subordinate.getBaseDN(); 1067 1068 // If the new search scope is 'base' and the search base DN does not 1069 // map the subordinate workflow then skip the subordinate workflow. 1070 if (newScope == SearchScope.BASE_OBJECT && !subordinateDN.parent().equals(originalBaseDN)) 1071 { 1072 continue; 1073 } 1074 1075 // If the request base DN is not a subordinate of the subordinate 1076 // workflow base DN then do not search in the subordinate workflow. 1077 if (!originalBaseDN.isSuperiorOrEqualTo(subordinateDN)) 1078 { 1079 continue; 1080 } 1081 1082 // Set the new request base DN and do execute the 1083 // operation in the subordinate workflow. 1084 searchOp.setBaseDN(subordinateDN); 1085 execute(searchOp, subordinateDN); 1086 boolean sendReferenceEntry = searchResultCode.elaborateGlobalResultCode( 1087 searchOp.getResultCode(), searchOp.getErrorMessage()); 1088 if (sendReferenceEntry) 1089 { 1090 // TODO jdemendi - turn a referral result code into a reference entry 1091 // and send the reference entry to the client application 1092 } 1093 } 1094 1095 // Now we are done with the operation, let's restore the original 1096 // base DN and search scope in the operation. 1097 searchOp.setBaseDN(originalBaseDN); 1098 searchOp.setScope(originalScope); 1099 1100 // Update the operation result code and error message 1101 searchOp.setResultCode(searchResultCode.resultCode); 1102 searchOp.setErrorMessage(searchResultCode.errorMessage); 1103 } 1104 1105 private static Collection<LocalBackendWorkflowElement> getSubordinates(LocalBackendWorkflowElement workflow) 1106 { 1107 final DN baseDN = workflow.getBaseDN(); 1108 final Backend<?> backend = workflow.getBackend(); 1109 1110 final ArrayList<LocalBackendWorkflowElement> results = new ArrayList<>(); 1111 for (Backend<?> subordinate : backend.getSubordinateBackends()) 1112 { 1113 for (DN subordinateDN : subordinate.getBaseDNs()) 1114 { 1115 if (subordinateDN.isSubordinateOrEqualTo(baseDN)) 1116 { 1117 results.add(registeredLocalBackends.get(subordinateDN)); 1118 } 1119 } 1120 } 1121 return results; 1122 } 1123 1124 /** 1125 * Elaborates a new search scope according to the current search scope. The 1126 * new scope is intended to be used for searches on subordinate workflows. 1127 * 1128 * @param currentScope 1129 * the current search scope 1130 * @return the new scope to use for searches on subordinate workflows, 1131 * <code>null</code> when current scope is 'base' 1132 */ 1133 private static SearchScope elaborateScopeForSearchInSubordinates(SearchScope currentScope) 1134 { 1135 switch (currentScope.asEnum()) 1136 { 1137 case BASE_OBJECT: 1138 return null; 1139 case SINGLE_LEVEL: 1140 return SearchScope.BASE_OBJECT; 1141 case SUBORDINATES: 1142 case WHOLE_SUBTREE: 1143 return SearchScope.WHOLE_SUBTREE; 1144 default: 1145 return currentScope; 1146 } 1147 } 1148 1149 static DN findMatchedDN(DN entryDN) 1150 { 1151 try 1152 { 1153 DN matchedDN = DirectoryServer.getParentDNInSuffix(entryDN); 1154 while (matchedDN != null) 1155 { 1156 if (DirectoryServer.entryExists(matchedDN)) 1157 { 1158 return matchedDN; 1159 } 1160 1161 matchedDN = DirectoryServer.getParentDNInSuffix(matchedDN); 1162 } 1163 } 1164 catch (Exception e) 1165 { 1166 logger.traceException(e); 1167 } 1168 return null; 1169 } 1170 1171 @Override 1172 public String toString() 1173 { 1174 return getClass().getSimpleName() 1175 + " backend=" + this.backend 1176 + " baseDN=" + this.baseDN; 1177 } 1178}