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}