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