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}