| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | |
| | | 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; |
| | | } |
| | |
| | | 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 |
| | |
| | | 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 |
| | | { |
| | |
| | | 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()) |
| | | { |
| | |
| | | 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) |
| | |
| | | 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()]; |