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-2009 Sun Microsystems, Inc. 025 * Portions Copyright 2011-2015 ForgeRock AS 026 */ 027package org.opends.server.workflowelement.localbackend; 028 029import java.util.List; 030import java.util.concurrent.atomic.AtomicBoolean; 031 032import org.forgerock.i18n.LocalizableMessage; 033import org.forgerock.i18n.slf4j.LocalizedLogger; 034import org.forgerock.opendj.ldap.ResultCode; 035import org.opends.server.api.AccessControlHandler; 036import org.opends.server.api.Backend; 037import org.opends.server.api.ClientConnection; 038import org.opends.server.api.SynchronizationProvider; 039import org.opends.server.controls.LDAPAssertionRequestControl; 040import org.opends.server.controls.LDAPPreReadRequestControl; 041import org.opends.server.core.AccessControlConfigManager; 042import org.opends.server.core.DeleteOperation; 043import org.opends.server.core.DeleteOperationWrapper; 044import org.opends.server.core.DirectoryServer; 045import org.opends.server.core.PersistentSearch; 046import org.opends.server.types.CanceledOperationException; 047import org.opends.server.types.Control; 048import org.opends.server.types.DN; 049import org.opends.server.types.DirectoryException; 050import org.opends.server.types.Entry; 051import org.opends.server.types.LockManager.DNLock; 052import org.opends.server.types.SearchFilter; 053import org.opends.server.types.SynchronizationProviderResult; 054import org.opends.server.types.operation.PostOperationDeleteOperation; 055import org.opends.server.types.operation.PostResponseDeleteOperation; 056import org.opends.server.types.operation.PostSynchronizationDeleteOperation; 057import org.opends.server.types.operation.PreOperationDeleteOperation; 058 059import static org.opends.messages.CoreMessages.*; 060import static org.opends.server.core.DirectoryServer.*; 061import static org.opends.server.types.AbstractOperation.*; 062import static org.opends.server.util.ServerConstants.*; 063import static org.opends.server.util.StaticUtils.*; 064 065/** 066 * This class defines an operation used to delete an entry in a local backend 067 * of the Directory Server. 068 */ 069public class LocalBackendDeleteOperation 070 extends DeleteOperationWrapper 071 implements PreOperationDeleteOperation, PostOperationDeleteOperation, 072 PostResponseDeleteOperation, 073 PostSynchronizationDeleteOperation 074{ 075 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 076 077 /** The backend in which the operation is to be processed. */ 078 private Backend<?> backend; 079 080 /** Indicates whether the LDAP no-op control has been requested. */ 081 private boolean noOp; 082 083 /** The client connection on which this operation was requested. */ 084 private ClientConnection clientConnection; 085 086 /** The DN of the entry to be deleted. */ 087 private DN entryDN; 088 089 /** The entry to be deleted. */ 090 private Entry entry; 091 092 /** The pre-read request control included in the request, if applicable. */ 093 private LDAPPreReadRequestControl preReadRequest; 094 095 096 097 /** 098 * Creates a new operation that may be used to delete an entry from a 099 * local backend of the Directory Server. 100 * 101 * @param delete The operation to enhance. 102 */ 103 public LocalBackendDeleteOperation(DeleteOperation delete) 104 { 105 super(delete); 106 LocalBackendWorkflowElement.attachLocalOperation (delete, this); 107 } 108 109 110 111 /** 112 * Retrieves the entry to be deleted. 113 * 114 * @return The entry to be deleted, or <CODE>null</CODE> if the entry is not 115 * yet available. 116 */ 117 @Override 118 public Entry getEntryToDelete() 119 { 120 return entry; 121 } 122 123 124 125 /** 126 * Process this delete operation in a local backend. 127 * 128 * @param wfe 129 * The local backend work-flow element. 130 * @throws CanceledOperationException 131 * if this operation should be cancelled 132 */ 133 public void processLocalDelete(final LocalBackendWorkflowElement wfe) 134 throws CanceledOperationException 135 { 136 this.backend = wfe.getBackend(); 137 138 clientConnection = getClientConnection(); 139 140 // Check for a request to cancel this operation. 141 checkIfCanceled(false); 142 143 try 144 { 145 AtomicBoolean executePostOpPlugins = new AtomicBoolean(false); 146 processDelete(executePostOpPlugins); 147 148 // Invoke the post-operation or post-synchronization delete plugins. 149 if (isSynchronizationOperation()) 150 { 151 if (getResultCode() == ResultCode.SUCCESS) 152 { 153 getPluginConfigManager().invokePostSynchronizationDeletePlugins(this); 154 } 155 } 156 else if (executePostOpPlugins.get()) 157 { 158 if (!processOperationResult(this, getPluginConfigManager().invokePostOperationDeletePlugins(this))) 159 { 160 return; 161 } 162 } 163 } 164 finally 165 { 166 LocalBackendWorkflowElement.filterNonDisclosableMatchedDN(this); 167 } 168 169 // Register a post-response call-back which will notify persistent 170 // searches and change listeners. 171 if (getResultCode() == ResultCode.SUCCESS) 172 { 173 registerPostResponseCallback(new Runnable() 174 { 175 @Override 176 public void run() 177 { 178 for (PersistentSearch psearch : backend.getPersistentSearches()) 179 { 180 psearch.processDelete(entry); 181 } 182 } 183 }); 184 } 185 } 186 187 private void processDelete(AtomicBoolean executePostOpPlugins) 188 throws CanceledOperationException 189 { 190 // Process the entry DN to convert it from its raw form as provided by the 191 // client to the form required for the rest of the delete processing. 192 entryDN = getEntryDN(); 193 if (entryDN == null) 194 { 195 return; 196 } 197 198 /* 199 * Grab a write lock on the entry and its subtree in order to prevent concurrent updates to 200 * subordinate entries. 201 */ 202 final DNLock subtreeLock = DirectoryServer.getLockManager().tryWriteLockSubtree(entryDN); 203 try 204 { 205 if (subtreeLock == null) 206 { 207 setResultCode(ResultCode.BUSY); 208 appendErrorMessage(ERR_DELETE_CANNOT_LOCK_ENTRY.get(entryDN)); 209 return; 210 } 211 212 // Get the entry to delete. If it doesn't exist, then fail. 213 entry = backend.getEntry(entryDN); 214 if (entry == null) 215 { 216 setResultCode(ResultCode.NO_SUCH_OBJECT); 217 appendErrorMessage(ERR_DELETE_NO_SUCH_ENTRY.get(entryDN)); 218 219 setMatchedDN(findMatchedDN(entryDN)); 220 return; 221 } 222 223 if (!handleConflictResolution()) 224 { 225 return; 226 } 227 228 // Check to see if the client has permission to perform the delete. 229 230 // Check to see if there are any controls in the request. If so, then 231 // see if there is any special processing required. 232 handleRequestControls(); 233 234 // FIXME: for now assume that this will check all permission 235 // pertinent to the operation. This includes proxy authorization 236 // and any other controls specified. 237 238 // FIXME: earlier checks to see if the entry already exists may 239 // have already exposed sensitive information to the client. 240 try 241 { 242 if (!getAccessControlHandler().isAllowed(this)) 243 { 244 setResultCodeAndMessageNoInfoDisclosure(entry, 245 ResultCode.INSUFFICIENT_ACCESS_RIGHTS, 246 ERR_DELETE_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS.get(entryDN)); 247 return; 248 } 249 } 250 catch (DirectoryException e) 251 { 252 setResultCode(e.getResultCode()); 253 appendErrorMessage(e.getMessageObject()); 254 return; 255 } 256 257 // Check for a request to cancel this operation. 258 checkIfCanceled(false); 259 260 // If the operation is not a synchronization operation, 261 // invoke the pre-delete plugins. 262 if (!isSynchronizationOperation()) 263 { 264 executePostOpPlugins.set(true); 265 if (!processOperationResult(this, getPluginConfigManager().invokePreOperationDeletePlugins(this))) 266 { 267 return; 268 } 269 } 270 271 // Get the backend to use for the delete. If there is none, then fail. 272 if (backend == null) 273 { 274 setResultCode(ResultCode.NO_SUCH_OBJECT); 275 appendErrorMessage(ERR_DELETE_NO_SUCH_ENTRY.get(entryDN)); 276 return; 277 } 278 279 LocalBackendWorkflowElement.checkIfBackendIsWritable(backend, this, 280 entryDN, ERR_DELETE_SERVER_READONLY, ERR_DELETE_BACKEND_READONLY); 281 282 // The selected backend will have the responsibility of making sure that 283 // the entry actually exists and does not have any children (or possibly 284 // handling a subtree delete). But we will need to check if there are 285 // any subordinate backends that should stop us from attempting the 286 // delete. 287 for (Backend<?> b : backend.getSubordinateBackends()) 288 { 289 for (DN dn : b.getBaseDNs()) 290 { 291 if (dn.isDescendantOf(entryDN)) 292 { 293 setResultCodeAndMessageNoInfoDisclosure(entry, 294 ResultCode.NOT_ALLOWED_ON_NONLEAF, 295 ERR_DELETE_HAS_SUB_BACKEND.get(entryDN, dn)); 296 return; 297 } 298 } 299 } 300 301 // Actually perform the delete. 302 if (noOp) 303 { 304 setResultCode(ResultCode.NO_OPERATION); 305 appendErrorMessage(INFO_DELETE_NOOP.get()); 306 } 307 else 308 { 309 if (!processPreOperation()) 310 { 311 return; 312 } 313 backend.deleteEntry(entryDN, this); 314 } 315 316 LocalBackendWorkflowElement.addPreReadResponse(this, preReadRequest, 317 entry); 318 319 if (!noOp) 320 { 321 setResultCode(ResultCode.SUCCESS); 322 } 323 } 324 catch (DirectoryException de) 325 { 326 logger.traceException(de); 327 328 setResponseData(de); 329 } 330 finally 331 { 332 if (subtreeLock != null) 333 { 334 subtreeLock.unlock(); 335 } 336 processSynchPostOperationPlugins(); 337 } 338 } 339 340 private AccessControlHandler<?> getAccessControlHandler() 341 { 342 return AccessControlConfigManager.getInstance().getAccessControlHandler(); 343 } 344 345 private DirectoryException newDirectoryException(Entry entry, 346 ResultCode resultCode, LocalizableMessage message) throws DirectoryException 347 { 348 return LocalBackendWorkflowElement.newDirectoryException(this, entry, 349 entryDN, 350 resultCode, message, ResultCode.NO_SUCH_OBJECT, 351 ERR_DELETE_NO_SUCH_ENTRY.get(entryDN)); 352 } 353 354 private void setResultCodeAndMessageNoInfoDisclosure(Entry entry, 355 ResultCode resultCode, LocalizableMessage message) throws DirectoryException 356 { 357 LocalBackendWorkflowElement.setResultCodeAndMessageNoInfoDisclosure(this, 358 entry, entryDN, resultCode, message, ResultCode.NO_SUCH_OBJECT, 359 ERR_DELETE_NO_SUCH_ENTRY.get(entryDN)); 360 } 361 362 private DN findMatchedDN(DN entryDN) 363 { 364 try 365 { 366 DN matchedDN = entryDN.getParentDNInSuffix(); 367 while (matchedDN != null) 368 { 369 if (DirectoryServer.entryExists(matchedDN)) 370 { 371 return matchedDN; 372 } 373 374 matchedDN = matchedDN.getParentDNInSuffix(); 375 } 376 } 377 catch (Exception e) 378 { 379 logger.traceException(e); 380 } 381 return null; 382 } 383 384 /** 385 * Performs any request control processing needed for this operation. 386 * 387 * @throws DirectoryException If a problem occurs that should cause the 388 * operation to fail. 389 */ 390 private void handleRequestControls() throws DirectoryException 391 { 392 LocalBackendWorkflowElement.evaluateProxyAuthControls(this); 393 LocalBackendWorkflowElement.removeAllDisallowedControls(entryDN, this); 394 395 List<Control> requestControls = getRequestControls(); 396 if (requestControls != null && !requestControls.isEmpty()) 397 { 398 for (Control c : requestControls) 399 { 400 final String oid = c.getOID(); 401 if (OID_LDAP_ASSERTION.equals(oid)) 402 { 403 LDAPAssertionRequestControl assertControl = 404 getRequestControl(LDAPAssertionRequestControl.DECODER); 405 406 SearchFilter filter; 407 try 408 { 409 filter = assertControl.getSearchFilter(); 410 } 411 catch (DirectoryException de) 412 { 413 logger.traceException(de); 414 415 throw newDirectoryException(entry, de.getResultCode(), 416 ERR_DELETE_CANNOT_PROCESS_ASSERTION_FILTER.get(entryDN, de.getMessageObject())); 417 } 418 419 // Check if the current user has permission to make this determination. 420 if (!getAccessControlHandler().isAllowed(this, entry, filter)) 421 { 422 throw new DirectoryException( 423 ResultCode.INSUFFICIENT_ACCESS_RIGHTS, 424 ERR_CONTROL_INSUFFICIENT_ACCESS_RIGHTS.get(oid)); 425 } 426 427 try 428 { 429 if (!filter.matchesEntry(entry)) 430 { 431 throw newDirectoryException(entry, ResultCode.ASSERTION_FAILED, 432 ERR_DELETE_ASSERTION_FAILED.get(entryDN)); 433 } 434 } 435 catch (DirectoryException de) 436 { 437 if (de.getResultCode() == ResultCode.ASSERTION_FAILED) 438 { 439 throw de; 440 } 441 442 logger.traceException(de); 443 444 throw newDirectoryException(entry, de.getResultCode(), 445 ERR_DELETE_CANNOT_PROCESS_ASSERTION_FILTER.get(entryDN, de.getMessageObject())); 446 } 447 } 448 else if (OID_LDAP_NOOP_OPENLDAP_ASSIGNED.equals(oid)) 449 { 450 noOp = true; 451 } 452 else if (OID_LDAP_READENTRY_PREREAD.equals(oid)) 453 { 454 preReadRequest = 455 getRequestControl(LDAPPreReadRequestControl.DECODER); 456 } 457 else if (LocalBackendWorkflowElement.isProxyAuthzControl(oid)) 458 { 459 continue; 460 } 461 // NYI -- Add support for additional controls. 462 else if (c.isCritical() 463 && (backend == null || !backend.supportsControl(oid))) 464 { 465 throw newDirectoryException(entry, 466 ResultCode.UNAVAILABLE_CRITICAL_EXTENSION, 467 ERR_DELETE_UNSUPPORTED_CRITICAL_CONTROL.get(entryDN, oid)); 468 } 469 } 470 } 471 } 472 473 private DN getName(Entry e) 474 { 475 return e != null ? e.getName() : DN.rootDN(); 476 } 477 478 /** 479 * Handle conflict resolution. 480 * @return {@code true} if processing should continue for the operation, or 481 * {@code false} if not. 482 */ 483 private boolean handleConflictResolution() { 484 for (SynchronizationProvider<?> provider : getSynchronizationProviders()) { 485 try { 486 SynchronizationProviderResult result = 487 provider.handleConflictResolution(this); 488 if (! result.continueProcessing()) { 489 setResultCodeAndMessageNoInfoDisclosure(entry, 490 result.getResultCode(), result.getErrorMessage()); 491 setMatchedDN(result.getMatchedDN()); 492 setReferralURLs(result.getReferralURLs()); 493 return false; 494 } 495 } catch (DirectoryException de) { 496 logger.traceException(de); 497 logger.error(ERR_DELETE_SYNCH_CONFLICT_RESOLUTION_FAILED, 498 getConnectionID(), getOperationID(), getExceptionMessage(de)); 499 setResponseData(de); 500 return false; 501 } 502 } 503 return true; 504 } 505 506 /** 507 * Invoke post operation synchronization providers. 508 */ 509 private void processSynchPostOperationPlugins() { 510 for (SynchronizationProvider<?> provider : getSynchronizationProviders()) { 511 try { 512 provider.doPostOperation(this); 513 } catch (DirectoryException de) { 514 logger.traceException(de); 515 logger.error(ERR_DELETE_SYNCH_POSTOP_FAILED, getConnectionID(), 516 getOperationID(), getExceptionMessage(de)); 517 setResponseData(de); 518 return; 519 } 520 } 521 } 522 523 /** 524 * Process pre operation. 525 * @return {@code true} if processing should continue for the operation, or 526 * {@code false} if not. 527 */ 528 private boolean processPreOperation() { 529 for (SynchronizationProvider<?> provider : getSynchronizationProviders()) { 530 try { 531 if (!processOperationResult(this, provider.doPreOperation(this))) { 532 return false; 533 } 534 } catch (DirectoryException de) { 535 logger.traceException(de); 536 logger.error(ERR_DELETE_SYNCH_PREOP_FAILED, getConnectionID(), 537 getOperationID(), getExceptionMessage(de)); 538 setResponseData(de); 539 return false; 540 } 541 } 542 return true; 543 } 544}