mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valera V Harseko
21 hours ago de42bdce8ff329998ad4eef467503ea3a4fafd89
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java
@@ -33,10 +33,12 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@@ -51,6 +53,7 @@
import org.forgerock.opendj.config.server.ConfigurationAddListener;
import org.forgerock.opendj.config.server.ConfigurationChangeListener;
import org.forgerock.opendj.config.server.ConfigurationDeleteListener;
import org.forgerock.opendj.ldap.AttributeDescription;
import org.forgerock.opendj.ldap.ByteSequence;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ByteStringBuilder;
@@ -64,6 +67,7 @@
import org.forgerock.opendj.server.config.server.PluggableBackendCfg;
import org.forgerock.util.Pair;
import org.opends.messages.CoreMessages;
import org.opends.server.api.AccessControlHandler;
import org.opends.server.api.ClientConnection;
import org.opends.server.api.EntryCache;
import org.opends.server.api.VirtualAttributeProvider;
@@ -85,6 +89,7 @@
import org.opends.server.controls.SubtreeDeleteControl;
import org.opends.server.controls.VLVRequestControl;
import org.opends.server.controls.VLVResponseControl;
import org.opends.server.core.AccessControlConfigManager;
import org.opends.server.core.AddOperation;
import org.opends.server.core.DeleteOperation;
import org.opends.server.core.DirectoryServer;
@@ -103,6 +108,7 @@
import org.opends.server.types.Operation;
import org.opends.server.types.Privilege;
import org.opends.server.types.SearchFilter;
import org.opends.server.types.SearchResultEntry;
import org.opends.server.types.VirtualAttributeRule;
import org.opends.server.util.ServerConstants;
import org.opends.server.util.StaticUtils;
@@ -955,16 +961,45 @@
          EntryIDSet candidateEntryIDs = null;
          boolean candidatesAreInScope = false;
          final AccessControlHandler<?> aciHandler = sortRequest != null ? sortAccessControl(searchOperation) : null;
          boolean sortedByVLVIndex = false;
          if (sortRequest != null)
          {
            for (VLVIndex vlvIndex : vlvIndexMap.values())
            {
              try
              {
                candidateEntryIDs = vlvIndex.evaluate(txn, searchOperation, sortRequest, vlvRequest, debugBuffer);
                if (aciHandler == null)
                {
                  candidateEntryIDs = vlvIndex.evaluate(txn, searchOperation, sortRequest, vlvRequest, debugBuffer);
                  sortedByVLVIndex = candidateEntryIDs != null;
                  if (sortedByVLVIndex)
                  {
                    addServerSideSortControl(searchOperation, SUCCESS);
                  }
                }
                else if (vlvIndex.isApplicable(searchOperation, sortRequest))
                {
                  // A VLV index answers from its stored keys alone, so it cannot tell which entries and sort
                  // values the caller may read. For a caller subject to access control, only take the entries of
                  // the index from it, and let the sort below order them through the caller's access controls
                  // (GHSA-6f6q-q9gw-4jr4).
                  if (refuseOverEntryLimit(vlvIndex.getEntryCount(txn), searchOperation, vlvRequest))
                  {
                    return null;
                  }
                  candidateEntryIDs = getEntriesToSort(txn, vlvIndex, searchOperation, sortRequest, debugBuffer);
                  // The index may have grown since it was counted: the sort must not read more entries than may be
                  // examined. An index grown over the most entries read from it at once gives an undefined set, which
                  // no limit admits.
                  if (candidateEntryIDs != null
                      && refuseOverEntryLimit(candidateEntryIDs.size(), searchOperation, vlvRequest))
                  {
                    return null;
                  }
                }
                if (candidateEntryIDs != null)
                {
                  addServerSideSortControl(searchOperation, SUCCESS);
                  candidatesAreInScope = true;
                  break;
                }
@@ -1009,53 +1044,50 @@
                candidatesAreInScope = true;
              }
            }
          }
            if (sortRequest != null)
            {
              // If the sort key is not present, the sorting will generate the
              // default ordering. VLV search request goes through as if
              // this sort key was not found in the user entry.
              try
              {
                List<SortKey> sortKeys = sortRequest.getSortKeys();
                reorderedCandidateEntryIDs = sort(txn, candidateEntryIDs, searchOperation, sortKeys, vlvRequest);
              }
              catch (DirectoryException de)
              {
                reorderedCandidateEntryIDs = candidateEntryIDs.toLongArray();
                serverSideSortControlError(searchOperation, sortRequest, de);
              }
              try
              {
                if (sortRequest.containsSortKeys())
                {
                  addServerSideSortControl(searchOperation, SUCCESS);
                }
                else
                {
                  /*
                   * There is no sort key associated with the sort control.
                   * Since it came here it means that the criticality is false
                   * so let the server return all search results unsorted and
                   * include the sortKeyResponseControl in the searchResultDone
                   * message.
                   */
                  addServerSideSortControl(searchOperation, NO_SUCH_ATTRIBUTE);
                }
              }
              catch (DirectoryException de)
              {
                serverSideSortControlError(searchOperation, sortRequest, de);
              }
            }
            else
            {
              reorderedCandidateEntryIDs = candidateEntryIDs.toLongArray();
            }
          if (sortedByVLVIndex || sortRequest == null)
          {
            reorderedCandidateEntryIDs = candidateEntryIDs.toLongArray();
          }
          else
          {
            reorderedCandidateEntryIDs = candidateEntryIDs.toLongArray();
            // If the sort key is not present, the sorting will generate the
            // default ordering. VLV search request goes through as if
            // this sort key was not found in the user entry.
            try
            {
              List<SortKey> sortKeys = sortRequest.getSortKeys();
              reorderedCandidateEntryIDs =
                  sort(txn, candidateEntryIDs, searchOperation, sortKeys, vlvRequest, aciHandler);
            }
            catch (DirectoryException de)
            {
              reorderedCandidateEntryIDs = candidateEntryIDs.toLongArray();
              serverSideSortControlError(searchOperation, sortRequest, de);
            }
            try
            {
              if (sortRequest.containsSortKeys())
              {
                addServerSideSortControl(searchOperation, SUCCESS);
              }
              else
              {
                /*
                 * There is no sort key associated with the sort control.
                 * Since it came here it means that the criticality is false
                 * so let the server return all search results unsorted and
                 * include the sortKeyResponseControl in the searchResultDone
                 * message.
                 */
                addServerSideSortControl(searchOperation, NO_SUCH_ATTRIBUTE);
              }
            }
            catch (DirectoryException de)
            {
              serverSideSortControlError(searchOperation, sortRequest, de);
            }
          }
          // If requested, construct and return a fictitious entry containing
@@ -1134,6 +1166,64 @@
          addPagedResultsControl(searchOperation, pageRequest, null);
        }
        /**
         * Refuses the search, and returns {@code true}, if the entries of a VLV index, as counted or as read, are more
         * than may be examined for a caller subject to access control: sorting them through its access controls reads
         * every one of them.
         */
        private boolean refuseOverEntryLimit(long entryCount, SearchOperation searchOperation,
            VLVRequestControl vlvRequest)
        {
          final int lookthroughLimit = searchOperation.getClientConnection().getLookthroughLimit();
          final int entryLimit =
              lookthroughLimit > 0 ? Math.min(lookthroughLimit, CURSOR_ENTRY_LIMIT) : CURSOR_ENTRY_LIMIT;
          if (entryCount <= entryLimit)
          {
            return false;
          }
          searchOperation.setResultCode(ResultCode.ADMIN_LIMIT_EXCEEDED);
          searchOperation.appendErrorMessage(NOTE_LOOKTHROUGH_LIMIT_EXCEEDED.get(entryLimit));
          addServerSideSortControl(searchOperation, ADMIN_LIMIT_EXCEEDED);
          if (vlvRequest != null)
          {
            addVLVResponseControl(searchOperation, 0, 0, ADMIN_LIMIT_EXCEEDED);
          }
          return true;
        }
        /**
         * Returns the entries of a VLV index, in entry ID order, for the sort of a caller subject to access control.
         * An index grown over the most entries read from it at once since it was checked gives an undefined set,
         * which the caller refuses as it refuses an index already counting that many. An index that gives no entries
         * otherwise, having stopped being applicable or no longer holding the entries it counts, gives {@code null}:
         * the search then goes on as without it.
         */
        private EntryIDSet getEntriesToSort(ReadableTransaction txn, VLVIndex vlvIndex,
            SearchOperation searchOperation, ServerSideSortRequestControl sortRequest, StringBuilder debugBuffer)
            throws DirectoryException
        {
          // Without a VLV request, the index gives all its entries, but nothing when it holds none.
          final EntryIDSet entryIDs = vlvIndex.evaluate(txn, searchOperation, sortRequest, null, debugBuffer);
          if (entryIDs == null)
          {
            if (!vlvIndex.isApplicable(searchOperation, sortRequest))
            {
              return null;
            }
            final int entryCount = vlvIndex.getEntryCount(txn);
            if (entryCount == 0)
            {
              return EntryIDSet.newDefinedSet();
            }
            return entryCount > CURSOR_ENTRY_LIMIT ? EntryIDSet.newUndefinedSet() : null;
          }
          // They come in the order of the stored keys, which the caller may not read: no road below, the fallback
          // after a sort error included, may hand them out in that order.
          final long[] ids = entryIDs.toLongArray();
          Arrays.sort(ids);
          return EntryIDSet.newDefinedSet(ids);
        }
        private void serverSideSortControlError(final SearchOperation searchOperation,
            ServerSideSortRequestControl sortRequest, DirectoryException de) throws DirectoryException
        {
@@ -2788,8 +2878,17 @@
    return baseEntry;
  }
  /**
   * Sorts the candidate entries of a search, then applies its VLV request if any.
   * <p>
   * Unless {@code aciHandler} is {@code null}, the sort goes through the caller's access controls: an entry the
   * caller may not see takes no part in it, and a sort attribute the caller may not read on an entry sorts as if
   * the entry had no value for it, so that neither the order nor the VLV target position or content count disclose
   * them (GHSA-6f6q-q9gw-4jr4).
   */
  private long[] sort(ReadableTransaction txn, EntryIDSet entryIDSet, SearchOperation searchOperation,
      List<SortKey> sortKeys, VLVRequestControl vlvRequest) throws DirectoryException
      List<SortKey> sortKeys, VLVRequestControl vlvRequest, AccessControlHandler<?> aciHandler)
      throws DirectoryException
  {
    if (!entryIDSet.isDefined())
    {
@@ -2806,9 +2905,14 @@
      try
      {
        Entry e = getEntry(txn, id);
        if (e.matchesBaseAndScope(baseDN, scope) && filter.matchesEntry(e))
        // The entry may have been deleted since the candidates were read, as searchIndexed() also expects.
        if (e != null && e.matchesBaseAndScope(baseDN, scope) && filter.matchesEntry(e))
        {
          sortMap.put(encodeVLVKey(sortKeys, e, id.longValue()), id);
          final ByteString vlvKey = encodeVisibleVLVKey(searchOperation, aciHandler, sortKeys, e, id.longValue());
          if (vlvKey != null)
          {
            sortMap.put(vlvKey, id);
          }
        }
      }
      catch (Exception e)
@@ -2832,6 +2936,73 @@
    return sortByGreaterThanOrEqualAssertion(searchOperation, vlvRequest, sortKeys, sortMap);
  }
  /**
   * Returns the access controls the sort of a search must go through, or {@code null} if they do not apply to it:
   * access control is disabled, or the caller holds the bypass-acl privilege, as internal operations do.
   */
  private static AccessControlHandler<?> sortAccessControl(SearchOperation searchOperation)
  {
    final AccessControlConfigManager accessControl = AccessControlConfigManager.getInstance();
    if (!accessControl.isAccessControlEnabled()
        || searchOperation.getClientConnection().hasPrivilege(Privilege.BYPASS_ACL, searchOperation))
    {
      return null;
    }
    return accessControl.getAccessControlHandler();
  }
  /**
   * Returns the VLV key of an entry as the caller may see it, or {@code null} if the caller may not see the entry.
   * <p>
   * Without access controls, the key is that of the entry. Otherwise the entry must pass the check a returned entry
   * passes, and the values of the attribute types the caller may not read on it are left out of the key. The entry
   * is the one read from the backend, with its virtual attributes, as a returned entry is.
   */
  private static ByteString encodeVisibleVLVKey(SearchOperation searchOperation, AccessControlHandler<?> aciHandler,
      List<SortKey> sortKeys, Entry entry, long entryID)
  {
    if (aciHandler == null)
    {
      return encodeVLVKey(sortKeys, entry, entryID);
    }
    if (!aciHandler.maySend(searchOperation, new SearchResultEntry(entry)))
    {
      return null;
    }
    // A sort key takes its values from its attribute type and from the subtypes of it on the entry, and access
    // control applies to each of these types on its own.
    final Set<AttributeType> checkedTypes = new HashSet<>();
    final Set<AttributeType> unreadableTypes = new HashSet<>();
    for (SortKey sortKey : sortKeys)
    {
      final AttributeType sortType = AttributeDescription.valueOf(sortKey.getAttributeDescription()).getAttributeType();
      for (Attribute attribute : entry.getAllAttributes(sortType))
      {
        final AttributeType type = attribute.getAttributeDescription().getAttributeType();
        if (checkedTypes.add(type) && !mayRead(searchOperation, aciHandler, entry, type))
        {
          unreadableTypes.add(type);
        }
      }
    }
    return encodeVLVKey(sortKeys, entry, entryID, unreadableTypes);
  }
  /** Indicates whether the caller may read an attribute type on an entry, checked as the assertion control does. */
  private static boolean mayRead(SearchOperation searchOperation, AccessControlHandler<?> aciHandler, Entry entry,
      AttributeType attributeType)
  {
    try
    {
      return aciHandler.isAllowed(searchOperation, entry, SearchFilter.createPresenceFilter(attributeType));
    }
    catch (DirectoryException e)
    {
      logger.traceException(e);
      return false;
    }
  }
  private static final long[] toArray(Collection<EntryID> entryIDs)
  {
    final long[] array = new long[entryIDs.size()];