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