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