16 files modified
2 files added
| | |
| | | |
| | | #COPY opendj-*.zip $OPENDJ_DIST_FILENAME |
| | | |
| | | RUN apt-get update \ |
| | | RUN printf 'Acquire::ForceIPv4 "true";\nAcquire::Retries "5";\n' > /etc/apt/apt.conf.d/99-ci-network \ |
| | | && apt-get update \ |
| | | && apt-get install -y --no-install-recommends curl unzip \ |
| | | && if [ -z "$VERSION" ] ; then VERSION="$(curl -i -o - --silent https://api.github.com/repos/OpenIdentityPlatform/OpenDJ/releases/latest | grep -m1 "\"name\"" | cut -d\" -f4)"; fi \ |
| | | && if [ ! -f "$OPENDJ_DIST_FILENAME" ]; then echo file exists && curl -L https://github.com/OpenIdentityPlatform/OpenDJ/releases/download/$VERSION/opendj-$VERSION.zip --output $OPENDJ_DIST_FILENAME; fi \ |
| | | && unzip $OPENDJ_DIST_FILENAME \ |
| | | && apt-get remove -y --purge curl unzip \ |
| | | && rm -rf /var/lib/apt/lists/* \ |
| | | && rm -rf /var/lib/apt/lists/* /etc/apt/apt.conf.d/99-ci-network \ |
| | | && rm -r $OPENDJ_DIST_FILENAME \ |
| | | && groupadd $OPENDJ_USER \ |
| | | && useradd -m -r -u 1001 -g $OPENDJ_USER $OPENDJ_USER \ |
| | |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * {@inheritDoc} |
| | | * <p> |
| | | * Only the base changelog entry can be retrieved by DN. Change records are streamed by |
| | | * {@link #search(SearchOperation)}, which needs the search filter and the change number range to |
| | | * position its cursors, so they cannot be looked up individually and {@code null} is returned for |
| | | * them. None of them is ever an alias, so callers dereferencing aliases get the answer they need. |
| | | */ |
| | | @Override |
| | | public Entry getEntry(final DN entryDN) throws DirectoryException |
| | | { |
| | |
| | | throw new DirectoryException(DirectoryServer.getCoreConfigManager().getServerErrorResultCode(), |
| | | ERR_BACKEND_GET_ENTRY_NULL.get(getBackendID())); |
| | | } |
| | | throw new RuntimeException("Not implemented"); |
| | | if (CHANGELOG_BASE_DN.equals(entryDN)) |
| | | { |
| | | return buildBaseChangelogEntry(); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | |
| | | * |
| | | * Copyright 2006-2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014-2016 ForgeRock AS. |
| | | * Portions Copyrighted 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.backends; |
| | | |
| | |
| | | SearchScope scope = searchOperation.getScope(); |
| | | SearchFilter filter = searchOperation.getFilter(); |
| | | |
| | | // Make sure the base entry exists if it's supposed to be in this backend. |
| | | // Make sure the base entry exists. A base DN this backend does not hold has no entry |
| | | // either, so it must be reported as missing rather than searched for. |
| | | Entry baseEntry = entryMap.get(baseDN); |
| | | if (baseEntry == null && handlesEntry(baseDN)) |
| | | if (baseEntry == null) |
| | | { |
| | | DN matchedDN = serverContext.getBackendConfigManager().getParentDNInSuffix(baseDN); |
| | | while (matchedDN != null) |
| | |
| | | ResultCode.NO_SUCH_OBJECT, message, matchedDN, null); |
| | | } |
| | | |
| | | if (baseEntry != null) |
| | | { |
| | | baseEntry = baseEntry.duplicate(true); |
| | | } |
| | | baseEntry = baseEntry.duplicate(true); |
| | | |
| | | // If it's a base-level search, then just get that entry and return it if it |
| | | // matches the filter. |
| | |
| | | * |
| | | * Copyright 2007-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2013-2016 ForgeRock AS. |
| | | * Portions Copyright 2025 3A Systems, LLC |
| | | * Portions Copyright 2025-2026 3A Systems, LLC |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | |
| | | */ |
| | | private EntryContainer accessBegin(Operation operation, DN entryDN) throws DirectoryException |
| | | { |
| | | return accessBegin(operation, entryDN, ResultCode.UNDEFINED); |
| | | } |
| | | |
| | | /** |
| | | * Begin a Backend API method that accesses the {@link EntryContainer} for <code>entryDN</code> |
| | | * and returns it. |
| | | * @param operation requesting the storage |
| | | * @param entryDN the target DN for the operation |
| | | * @param noEntryContainerResultCode the result code to report when this backend holds no |
| | | * entry container for <code>entryDN</code> |
| | | * @return <code>EntryContainer</code> where <code>entryDN</code> resides |
| | | */ |
| | | private EntryContainer accessBegin(Operation operation, DN entryDN, ResultCode noEntryContainerResultCode) |
| | | throws DirectoryException |
| | | { |
| | | checkRootContainerInitialized(); |
| | | rootContainer.checkForEnoughResources(operation); |
| | | EntryContainer ec = rootContainer.getEntryContainer(entryDN); |
| | | if (ec == null) |
| | | { |
| | | throw new DirectoryException(ResultCode.UNDEFINED, ERR_BACKEND_ENTRY_DOESNT_EXIST.get(entryDN, getBackendID())); |
| | | throw new DirectoryException( |
| | | noEntryContainerResultCode, ERR_BACKEND_ENTRY_DOESNT_EXIST.get(entryDN, getBackendID())); |
| | | } |
| | | threadTotalCount.getAndIncrement(); |
| | | return ec; |
| | |
| | | @Override |
| | | public void search(SearchOperation searchOperation) throws DirectoryException, CanceledOperationException |
| | | { |
| | | EntryContainer ec = accessBegin(searchOperation, searchOperation.getBaseDN()); |
| | | // a base DN held by no entry container of this backend does not exist as far as a client |
| | | // is concerned: report it as such instead of the UNDEFINED result code used internally. |
| | | EntryContainer ec = accessBegin(searchOperation, searchOperation.getBaseDN(), ResultCode.NO_SUCH_OBJECT); |
| | | |
| | | ec.sharedLock.lock(); |
| | | |
| | |
| | | * |
| | | * Copyright 2006-2009 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.core; |
| | | |
| | |
| | | boolean evaluateAci); |
| | | |
| | | /** |
| | | * Indicates that the search phase is over and that any further entry comes from a persistent |
| | | * search. State kept to dereference aliases during the search phase is released, and no further |
| | | * entry is matched against it: a persistent search must report every change it is notified of, |
| | | * whether or not the entry was returned by the search phase. |
| | | */ |
| | | void endSearchPhase(); |
| | | |
| | | /** |
| | | * Used as a callback for backends to indicate that the provided search |
| | | * reference was encountered during processing and that additional processing |
| | | * should be performed to potentially send it back to the client. |
| | |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2016 ForgeRock AS. |
| | | * Portions Copyright 2024 3A Systems, LLC. |
| | | * Portions Copyright 2024-2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.core; |
| | | |
| | | import java.util.*; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | import java.util.concurrent.atomic.AtomicBoolean; |
| | | |
| | | import org.forgerock.i18n.LocalizedIllegalArgumentException; |
| | |
| | | { |
| | | return returnEntry(entry, controls, true); |
| | | } |
| | | Set<DN> dereferenced=new HashSet<>(); |
| | | |
| | | /** |
| | | * The DNs of the entries already returned by the search phase. An alias may be dereferenced onto |
| | | * an entry which is in the scope of the search as well, and that entry must only be returned once. |
| | | * It is emptied once the search phase is over, because a persistent search must report every |
| | | * change it is notified of, whether or not the entry was returned before. |
| | | */ |
| | | private final Set<DN> returnedDNs = ConcurrentHashMap.newKeySet(); |
| | | |
| | | /** Whether the search phase is over and only persistent search notifications remain. */ |
| | | private volatile boolean searchPhaseOver; |
| | | |
| | | @Override |
| | | public final void endSearchPhase() |
| | | { |
| | | searchPhaseOver = true; |
| | | returnedDNs.clear(); |
| | | } |
| | | |
| | | @Override |
| | | public final boolean returnEntry(Entry entry, List<Control> controls, |
| | | boolean evaluateAci) |
| | | { |
| | | return returnEntry(entry, controls, evaluateAci, null); |
| | | } |
| | | |
| | | /** |
| | | * Returns the provided entry to the client, dereferencing it first if it is an alias and the |
| | | * deref policy asks for it. |
| | | * |
| | | * @param entry The entry to return. |
| | | * @param controls The controls to attach to the entry. |
| | | * @param evaluateAci Whether the access control handler must be consulted. |
| | | * @param aliasChain The DNs of the aliases already dereferenced on the way to this entry, or |
| | | * {@code null} if no alias was dereferenced yet. It only spans the current |
| | | * chain, so it cannot grow beyond the length of that chain. |
| | | * @return {@code true} if the search should continue, {@code false} if it should stop. |
| | | */ |
| | | private boolean returnEntry(Entry entry, List<Control> controls, |
| | | boolean evaluateAci, Set<DN> aliasChain) |
| | | { |
| | | boolean typesOnly = getTypesOnly(); |
| | | |
| | | // See if the size limit has been exceeded. If so, then don't send the |
| | |
| | | //DereferenceAliasesPolicy |
| | | if ( DereferenceAliasesPolicy.ALWAYS.equals(getDerefPolicy()) || DereferenceAliasesPolicy.IN_SEARCHING.equals(getDerefPolicy()) ) { |
| | | if (entry.isAlias() && !baseDN.equals(entry.getName())) { |
| | | try { |
| | | final DN dn = entry.getAliasedDN(); |
| | | final Entry dereference = DirectoryServer.getEntry(dn); |
| | | if (dereferenced.contains(dn)) { |
| | | return true; |
| | | } |
| | | dereferenced.add(dn); |
| | | return returnEntry(dereference, controls, true); |
| | | } catch (DirectoryException e) { |
| | | throw new RuntimeException(e); |
| | | } |
| | | return returnAliasedEntry(entry, controls, aliasChain); |
| | | } |
| | | if (dereferenced.contains(entry.getName())) { |
| | | if (!searchPhaseOver && !returnedDNs.add(entry.getName())) { |
| | | // This entry was already returned by the search, through an alias or on its own. |
| | | return true; |
| | | } |
| | | dereferenced.add(entry.getName()); |
| | | } |
| | | |
| | | // Make a copy of the entry and pare it down to only include the set |
| | |
| | | return pluginResult.continueProcessing(); |
| | | } |
| | | |
| | | /** |
| | | * Returns the entry the provided alias points to, in place of the alias itself. |
| | | * |
| | | * @param alias The alias entry to dereference. |
| | | * @param controls The controls to attach to the entry. |
| | | * @param aliasChain The DNs of the aliases already dereferenced on the way to this alias, or |
| | | * {@code null} if this alias is the first one of the chain. |
| | | * @return {@code true} if the search should continue, {@code false} if it should stop. |
| | | */ |
| | | private boolean returnAliasedEntry(Entry alias, List<Control> controls, Set<DN> aliasChain) |
| | | { |
| | | final DN aliasedDN; |
| | | final Entry aliasedEntry; |
| | | try |
| | | { |
| | | // getAliasedDN() parses the aliasedObjectName value, which throws the unchecked |
| | | // LocalizedIllegalArgumentException when that value is not a valid DN. |
| | | aliasedDN = alias.getAliasedDN(); |
| | | aliasedEntry = DirectoryServer.getEntry(aliasedDN); |
| | | } |
| | | catch (DirectoryException | LocalizedIllegalArgumentException e) |
| | | { |
| | | // A single alias which cannot be read must not fail the whole search. |
| | | logger.traceException(e); |
| | | return true; |
| | | } |
| | | |
| | | if (aliasedEntry == null) |
| | | { |
| | | // The alias points to an entry which does not exist: there is nothing to return for it. |
| | | return true; |
| | | } |
| | | if (!searchPhaseOver && returnedDNs.contains(aliasedDN)) |
| | | { |
| | | // The aliased entry was already returned by the search. |
| | | return true; |
| | | } |
| | | if (aliasChain == null) |
| | | { |
| | | aliasChain = new HashSet<>(); |
| | | } |
| | | if (!aliasChain.add(aliasedDN)) |
| | | { |
| | | // The aliases point at each other: stop before looping forever. |
| | | return true; |
| | | } |
| | | return returnEntry(aliasedEntry, controls, true, aliasChain); |
| | | } |
| | | |
| | | private AccessControlHandler<?> getACIHandler() |
| | | { |
| | | return AccessControlConfigManager.getInstance().getAccessControlHandler(); |
| | |
| | | * |
| | | * Copyright 2008-2009 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.core; |
| | | |
| | |
| | | } |
| | | |
| | | @Override |
| | | public void endSearchPhase() |
| | | { |
| | | getOperation().endSearchPhase(); |
| | | } |
| | | |
| | | @Override |
| | | public boolean returnReference(DN dn, SearchResultReference reference) |
| | | { |
| | | return getOperation().returnReference(dn, reference); |
| | |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC |
| | | */ |
| | | package org.opends.server.replication.server; |
| | | |
| | |
| | | { |
| | | // initializations |
| | | localGenerationId = -1; |
| | | oldGenerationId = -100; |
| | | |
| | | // processes the ServerStart message received |
| | | boolean sessionInitiatorSSLEncryption = |
| | |
| | | lockDomainNoTimeout(); |
| | | |
| | | localGenerationId = replicationServerDomain.getGenerationId(); |
| | | oldGenerationId = localGenerationId; |
| | | |
| | | if (replicationServerDomain.isAlreadyConnectedToDS(this)) |
| | | { |
| | |
| | | // WARNING: Must be done before computing topo message to send |
| | | // to peer server as topo message must embed valid generation id |
| | | // for our server |
| | | oldGenerationId = replicationServerDomain.changeGenerationId(generationId); |
| | | setDomainGenerationIdOnStart(generationId); |
| | | } |
| | | } |
| | | return startSessionMsg; |
| | |
| | | this.generationIdSavedStatus = false; |
| | | |
| | | // generationId gossip is purely event-driven: it only travels in the |
| | | // topology messages sent on connect/disconnect/status events. Re-advertise |
| | | // on every real transition so a peer that missed one converges on the next. |
| | | if (generationId > 0) |
| | | { |
| | | sendTopoInfoToAll(); |
| | | } |
| | | // topology messages sent on connect/disconnect/status events. |
| | | // Re-advertise on every real transition — including a reset or |
| | | // rollback to -1 — so peers that recorded the previous value converge |
| | | // instead of diverging until the next unrelated topology event. |
| | | sendTopoInfoToAll(); |
| | | } |
| | | return oldGenerationId; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Sets the provided value as the new in memory generationId, but only if the |
| | | * current generation id still equals {@code expectedGenerationId}: a |
| | | * decision made on a stale snapshot must never overwrite a value another |
| | | * thread legitimately set in the meantime. |
| | | * |
| | | * @param expectedGenerationId The generation id the caller based its |
| | | * decision on. |
| | | * @param newGenerationId The new value of generationId. |
| | | * @return whether the generation id was changed |
| | | */ |
| | | public boolean changeGenerationIdIfUnchanged(long expectedGenerationId, |
| | | long newGenerationId) |
| | | { |
| | | synchronized (generationIDLock) |
| | | { |
| | | if (this.generationId != expectedGenerationId) |
| | | { |
| | | return false; |
| | | } |
| | | changeGenerationId(newGenerationId); |
| | | return true; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Rolls the generation id back to {@code oldGenerationId}, but only if it |
| | | * still has the {@code expectedGenerationId} value the caller previously set. |
| | | * An aborting handshake must undo its own change without overwriting a value |
| | | * that another thread legitimately set in the meantime (e.g. adopted from a |
| | | * peer topology message). The rollback is also skipped once the generation |
| | | * id has been saved to the changelog or while data servers are connected — |
| | | * the same invariant {@link #resetGenerationIdIfPossible()} enforces — |
| | | * because rolling back would clear a changelog that now holds real changes. |
| | | * |
| | | * @param expectedGenerationId The generation id the caller set and expects |
| | | * to still be in place. |
| | | * @param oldGenerationId The value to restore. |
| | | */ |
| | | public void rollbackGenerationIdIfUnchanged(long expectedGenerationId, |
| | | long oldGenerationId) |
| | | { |
| | | synchronized (generationIDLock) |
| | | { |
| | | if (!generationIdSavedStatus && connectedDSs.isEmpty()) |
| | | { |
| | | changeGenerationIdIfUnchanged(expectedGenerationId, oldGenerationId); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Resets the generationID. |
| | | * |
| | | * @param senderHandler The handler associated to the server |
| | |
| | | |
| | | private void setGenerationIdIfUnset(long generationId) |
| | | { |
| | | if (this.generationId < 0) |
| | | synchronized (generationIDLock) |
| | | { |
| | | this.generationId = generationId; |
| | | if (this.generationId < 0) |
| | | { |
| | | this.generationId = generationId; |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC |
| | | */ |
| | | package org.opends.server.replication.server; |
| | | |
| | |
| | | // Only V2 protocol has the group id in repl server start message |
| | | this.groupId = inReplServerStartMsg.getGroupId(); |
| | | } |
| | | |
| | | oldGenerationId = -100; |
| | | } |
| | | catch(Exception e) |
| | | { |
| | |
| | | |
| | | setBaseDNAndDomain(baseDN, false); |
| | | |
| | | localGenerationId = replicationServerDomain.getGenerationId(); |
| | | oldGenerationId = localGenerationId; |
| | | |
| | | try |
| | | { |
| | | lockDomainNoTimeout(); |
| | | |
| | | // Read under the domain lock so the start message advertises any change |
| | | // made by a previous handshake (handshakes serialize on this lock). The |
| | | // field itself is only guarded by generationIDLock and lock-free |
| | | // adopters can still move it — the arming CAS in |
| | | // setDomainGenerationIdOnStart handles that residual race. |
| | | localGenerationId = replicationServerDomain.getGenerationId(); |
| | | |
| | | ReplServerStartMsg outReplServerStartMsg = sendStartToRemote(); |
| | | |
| | | // Wait answer |
| | |
| | | */ |
| | | if (localGenerationId < 0 && generationId > 0) |
| | | { |
| | | oldGenerationId = |
| | | replicationServerDomain.changeGenerationId(generationId); |
| | | setDomainGenerationIdOnStart(generationId); |
| | | } |
| | | |
| | | logStartHandshakeSNDandRCV(outReplServerStartMsg,(ReplServerStartMsg)msg); |
| | |
| | | public void startFromRemoteRS(ReplServerStartMsg inReplServerStartMsg) |
| | | { |
| | | localGenerationId = -1; |
| | | oldGenerationId = -100; |
| | | try |
| | | { |
| | | // The initiator decides if the session is encrypted |
| | |
| | | // The local RS is not initialized - take the one received |
| | | // WARNING: Must be done before computing topo message to send to peer |
| | | // server as topo message must embed valid generation id for our server |
| | | oldGenerationId = |
| | | replicationServerDomain.changeGenerationId(generationId); |
| | | setDomainGenerationIdOnStart(generationId); |
| | | return; |
| | | } |
| | | |
| | |
| | | protected long generationId = -1; |
| | | /** The generation id of the hosting RS. */ |
| | | protected long localGenerationId = -1; |
| | | /** The generation id before processing a new start handshake. */ |
| | | protected long oldGenerationId = -1; |
| | | /** |
| | | * The domain generation id that this handler replaced during the start |
| | | * handshake, or -100 when this handler has not changed the domain generation |
| | | * id and there is nothing to roll back on {@link #abortStart(LocalizableMessage)}. |
| | | */ |
| | | protected long oldGenerationId = -100; |
| | | /** |
| | | * The generation id this handler wrote into the domain during the start |
| | | * handshake, or -100 when it wrote none. Used by |
| | | * {@link #abortStart(LocalizableMessage)} to roll back only this handler's |
| | | * own change: a value concurrently set by another thread must not be |
| | | * overwritten with the stale {@link #oldGenerationId}. |
| | | */ |
| | | private long generationIdSetOnStart = -100; |
| | | /** Group id of this remote server. */ |
| | | protected byte groupId = -1; |
| | | /** The SSL encryption after the negotiation with the peer. */ |
| | |
| | | localSession.close(); |
| | | } |
| | | |
| | | releaseDomainLock(); |
| | | |
| | | // If generation id of domain was changed, set it back to old value |
| | | // We may have changed it as it was -1 and we received a value >0 from peer |
| | | // server and the last topo message sent may have failed being sent: in that |
| | | // case retrieve old value of generation id for replication server domain |
| | | // If this handler changed the domain generation id during the handshake, |
| | | // set it back to the old value. Only undo our own change: another thread |
| | | // may have legitimately changed the generation id in the meantime (e.g. |
| | | // adopted it from a peer topology message) and that value must not be |
| | | // overwritten with our stale snapshot. Do it BEFORE releasing the domain |
| | | // lock, so a handshake queued on the lock cannot read, advertise or arm on |
| | | // the doomed value in between. |
| | | if (oldGenerationId != -100) |
| | | { |
| | | replicationServerDomain.changeGenerationId(oldGenerationId); |
| | | replicationServerDomain.rollbackGenerationIdIfUnchanged( |
| | | generationIdSetOnStart, oldGenerationId); |
| | | oldGenerationId = -100; |
| | | generationIdSetOnStart = -100; |
| | | } |
| | | |
| | | releaseDomainLock(); |
| | | } |
| | | |
| | | /** |
| | | * Changes the domain generation id during the start handshake, remembering |
| | | * the replaced value so that {@link #abortStart(LocalizableMessage)} can |
| | | * undo this handler's own change (and only it) if the handshake |
| | | * subsequently fails. |
| | | * <p> |
| | | * The change is applied only if the domain generation id still equals |
| | | * {@link #localGenerationId}, the value this handler based its decision on: |
| | | * a generation id concurrently adopted by a lock-free path (an update |
| | | * received from an already connected server) must not be stomped. This also |
| | | * makes a repeated call within one handshake a no-op, preserving the first |
| | | * arming's rollback point. Wire values below -1 are rejected — they can only |
| | | * come from a malformed peer and would collide with the -100 sentinel. |
| | | * |
| | | * @param generationId the generation id to set on the domain |
| | | */ |
| | | protected void setDomainGenerationIdOnStart(long generationId) |
| | | { |
| | | if (generationId < -1) |
| | | { |
| | | return; |
| | | } |
| | | if (replicationServerDomain.changeGenerationIdIfUnchanged( |
| | | localGenerationId, generationId)) |
| | | { |
| | | if (oldGenerationId == -100) |
| | | { |
| | | oldGenerationId = localGenerationId; |
| | | } |
| | | generationIdSetOnStart = generationId; |
| | | } |
| | | } |
| | | |
| | |
| | | * |
| | | * Copyright 2008-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2016 ForgeRock AS. |
| | | * Portions Copyright 2024-2025 3A Systems, LLC. |
| | | * Portions Copyright 2024-2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.workflowelement.localbackend; |
| | | |
| | |
| | | if(!dereferencingDNs.contains(aliasedDn)) { //detect recursive search |
| | | dereferencingDNs.add(aliasedDn); |
| | | setBaseDN(aliasedDn); |
| | | // the alias may point into another backend: a null backend is reported |
| | | // as NO_SUCH_OBJECT by the recursive call |
| | | backend = DirectoryServer.getInstance().getServerContext() |
| | | .getBackendConfigManager().findLocalBackendForEntry(aliasedDn); |
| | | try { |
| | | processSearch(executePostOpPlugins); |
| | | } catch (StackOverflowError error) { |
| | |
| | | // Process the search in the backend and all its subordinates. |
| | | backend.search(this); |
| | | } |
| | | |
| | | // Any entry returned from now on comes from the persistent search, if there is one. |
| | | endSearchPhase(); |
| | | } |
| | | catch (DirectoryException de) |
| | | { |
| | |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2014-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.backends; |
| | | |
| | |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.DereferenceAliasesPolicy; |
| | | import org.forgerock.opendj.ldap.RDN; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.opendj.ldap.SearchScope; |
| | |
| | | debugInfo(testName, "Ending test successfully"); |
| | | } |
| | | |
| | | /** |
| | | * The alias dereferencing policies which make the server look the search base entry up before |
| | | * handing the search over to the backend. |
| | | */ |
| | | @DataProvider |
| | | Object[][] getBaseEntryLookupDerefPolicies() |
| | | { |
| | | return new Object[][] { |
| | | { DereferenceAliasesPolicy.ALWAYS }, |
| | | { DereferenceAliasesPolicy.FINDING_BASE }, |
| | | // only looks the base entry up for subtree searches, which newSearchRequest() uses |
| | | { DereferenceAliasesPolicy.IN_SEARCHING }, |
| | | }; |
| | | } |
| | | |
| | | /** |
| | | * Dereferencing aliases makes the server read the base entry before searching, so the changelog |
| | | * backend must be able to return it rather than failing the whole search. |
| | | * |
| | | * @see <a href="https://github.com/OpenIdentityPlatform/OpenDJ/issues/737">issue #737</a> |
| | | */ |
| | | @Test(dataProvider = "getBaseEntryLookupDerefPolicies") |
| | | public void searchWhenNoChangesShouldReturnRootEntryOnlyWhenDereferencingAliases( |
| | | DereferenceAliasesPolicy derefPolicy) throws Exception |
| | | { |
| | | String testName = "EmptyRSDeref/" + derefPolicy; |
| | | debugInfo(testName, "Starting test\n\n"); |
| | | |
| | | SearchRequest request = newSearchRequest("(objectclass=*)").setDereferenceAliasesPolicy(derefPolicy); |
| | | searchChangelog(request, 1, SUCCESS, testName); |
| | | |
| | | debugInfo(testName, "Ending test successfully"); |
| | | } |
| | | |
| | | /** |
| | | * Dereferencing aliases used to fail change number searches with an unexpected error, which broke |
| | | * every client defaulting to it, JNDI-based ones in particular. |
| | | * |
| | | * @see <a href="https://github.com/OpenIdentityPlatform/OpenDJ/issues/737">issue #737</a> |
| | | */ |
| | | @Test(dataProvider = "getBaseEntryLookupDerefPolicies") |
| | | public void searchInChangeNumberModeWhenDereferencingAliases(DereferenceAliasesPolicy derefPolicy) throws Exception |
| | | { |
| | | String testName = "ChangeNumberDeref/" + derefPolicy; |
| | | debugInfo(testName, "Starting test\n\n"); |
| | | |
| | | CSN[] csns = generateAndPublishUpdateMsgForEachOperationType(testName, false); |
| | | |
| | | SearchRequest request = newSearchRequest("(changenumber>=1)").setDereferenceAliasesPolicy(derefPolicy); |
| | | InternalSearchOperation searchOp = searchChangelog(request, csns.length, SUCCESS, testName); |
| | | |
| | | assertEntriesForEachOperationType(searchOp.getSearchEntries(), 1, testName, USER1_ENTRY_UUID, csns); |
| | | |
| | | debugInfo(testName, "Ending test successfully"); |
| | | } |
| | | |
| | | /** |
| | | * The base changelog entry is the only one the backend can return by DN: change records need the |
| | | * search parameters to be located, so they are only reachable through a search. |
| | | */ |
| | | @Test |
| | | public void getEntryShouldReturnBaseEntryOnly() throws Exception |
| | | { |
| | | String testName = "getEntry"; |
| | | debugInfo(testName, "Starting test\n\n"); |
| | | |
| | | generateAndPublishUpdateMsgForEachOperationType(testName, false); |
| | | |
| | | final LocalBackend<?> backend = TestCaseUtils.getServerContext().getBackendConfigManager() |
| | | .getLocalBackendById(ChangelogBackend.BACKEND_ID); |
| | | assertEquals(backend.getEntry(ChangelogBackend.CHANGELOG_BASE_DN).getName(), |
| | | ChangelogBackend.CHANGELOG_BASE_DN); |
| | | assertNull(backend.getEntry(DN.valueOf("changeNumber=1,cn=changelog"))); |
| | | assertNull(backend.getEntry(DN.valueOf("changeNumber=1000,cn=changelog"))); |
| | | |
| | | debugInfo(testName, "Ending test successfully"); |
| | | } |
| | | |
| | | @Test |
| | | public void operationalAndVirtualAttributesShouldNotBeVisibleOutsideRootDSE() throws Exception |
| | | { |
| New file |
| | |
| | | /* |
| | | * The contents of this file are subject to the terms of the Common Development and |
| | | * Distribution License (the License). You may not use this file except in compliance with the |
| | | * License. |
| | | * |
| | | * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the |
| | | * specific language governing permission and limitations under the License. |
| | | * |
| | | * When distributing Covered Software, include this CDDL Header Notice in each file and include |
| | | * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.backends; |
| | | |
| | | import static org.opends.server.protocols.internal.InternalClientConnection.getRootConnection; |
| | | import static org.opends.server.protocols.internal.Requests.newSearchRequest; |
| | | import static org.testng.Assert.assertEquals; |
| | | import static org.testng.Assert.fail; |
| | | |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.opendj.ldap.SearchScope; |
| | | import org.opends.server.TestCaseUtils; |
| | | import org.opends.server.protocols.internal.InternalSearchOperation; |
| | | import org.opends.server.protocols.internal.SearchRequest; |
| | | import org.opends.server.types.DirectoryException; |
| | | import org.opends.server.workflowelement.localbackend.LocalBackendSearchOperation; |
| | | import org.testng.annotations.BeforeClass; |
| | | import org.testng.annotations.Test; |
| | | |
| | | /** Test cases for the memory backend. */ |
| | | @SuppressWarnings("javadoc") |
| | | public class MemoryBackendTestCase extends BackendTestCase |
| | | { |
| | | private MemoryBackend backend; |
| | | |
| | | @BeforeClass |
| | | public void startServer() throws Exception |
| | | { |
| | | TestCaseUtils.startServer(); |
| | | TestCaseUtils.initializeTestBackend(true); |
| | | backend = (MemoryBackend) TestCaseUtils.getServerContext().getBackendConfigManager() |
| | | .getLocalBackendById(TestCaseUtils.TEST_BACKEND_ID); |
| | | } |
| | | |
| | | /** A base DN this backend does not hold has no entry either. */ |
| | | @Test |
| | | public void testSearchBaseDNNotHeldByBackend() throws Exception |
| | | { |
| | | assertSearchFailsWithNoSuchObject(DN.valueOf("o=not-held-by-this-backend")); |
| | | } |
| | | |
| | | /** A base DN this backend holds, but which has never been added. */ |
| | | @Test |
| | | public void testSearchBaseDNWithoutEntry() throws Exception |
| | | { |
| | | assertSearchFailsWithNoSuchObject(DN.valueOf("ou=missing,o=test")); |
| | | } |
| | | |
| | | private void assertSearchFailsWithNoSuchObject(DN baseDN) throws Exception |
| | | { |
| | | final SearchRequest request = newSearchRequest(baseDN, SearchScope.BASE_OBJECT); |
| | | final InternalSearchOperation search = new InternalSearchOperation(getRootConnection(), -1, -1, request); |
| | | try |
| | | { |
| | | backend.search(new LocalBackendSearchOperation(search)); |
| | | fail("Searching base DN " + baseDN + " should have failed."); |
| | | } |
| | | catch (DirectoryException e) |
| | | { |
| | | assertEquals(e.getResultCode(), ResultCode.NO_SUCH_OBJECT); |
| | | } |
| | | } |
| | | } |
| | |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2015-2016 ForgeRock AS. |
| | | * Portions Copyright 2023-2025 3A Systems, LLC. |
| | | * Portions Copyright 2023-2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.backends.pluggable; |
| | | |
| | |
| | | "Leaf entry should not have any subordinates."); |
| | | } |
| | | |
| | | /** A base DN held by no entry container of this backend has no entry either. */ |
| | | @Test |
| | | public void testSearchBaseDNNotHeldByBackend() throws Exception |
| | | { |
| | | try |
| | | { |
| | | backend.search(createSearchOperation( |
| | | DN.valueOf("dc=a"), SearchScope.BASE_OBJECT, "(objectClass=*)", new ArrayList<Entry>())); |
| | | fail("Searching a base DN not held by this backend should have failed."); |
| | | } |
| | | catch (DirectoryException e) |
| | | { |
| | | assertEquals(e.getResultCode(), ResultCode.NO_SUCH_OBJECT); |
| | | } |
| | | } |
| | | |
| | | private List<SearchResultEntry> runSearch(SearchRequest request, boolean useInternalConnection) throws Exception |
| | | { |
| | | InternalClientConnection conn = getRootConnection(); |
| | |
| | | if (server2 == null) |
| | | { |
| | | server2 = openReplicationSession(baseDN, |
| | | server2ID, 100, getReplServerPort(replServer1ID), 1000); |
| | | server2ID, 100, getReplServerPort(replServer1ID), 10000); |
| | | } |
| | | |
| | | // In S1 launch the total update |
| | |
| | | if (server2 == null) |
| | | { |
| | | server2 = openReplicationSession(baseDN, |
| | | server2ID, 100, getReplServerPort(replServer1ID), 1000); |
| | | server2ID, 100, getReplServerPort(replServer1ID), 10000); |
| | | } |
| | | |
| | | InitializeRequestMsg initMsg = new InitializeRequestMsg(baseDN, server2ID, server1ID, 100); |
| | |
| | | if (server2 == null) |
| | | { |
| | | server2 = openReplicationSession(baseDN, |
| | | server2ID, 100, getReplServerPort(replServer1ID), 1000); |
| | | server2ID, 100, getReplServerPort(replServer1ID), 10000); |
| | | } |
| | | |
| | | // Launch in S1 the task that will initialize S2 |
| | |
| | | if (server2 == null) |
| | | { |
| | | server2 = openReplicationSession(baseDN, |
| | | server2ID, 100, getReplServerPort(replServer1ID), 1000); |
| | | server2ID, 100, getReplServerPort(replServer1ID), 10000); |
| | | } |
| | | |
| | | if (server3 == null) |
| | | { |
| | | server3 = openReplicationSession(baseDN, |
| | | server3ID, 100, getReplServerPort(replServer1ID), 1000); |
| | | server3ID, 100, getReplServerPort(replServer1ID), 10000); |
| | | } |
| | | |
| | | // Launch in S1 the task that will initialize S2 |
| | |
| | | if (server2==null) |
| | | { |
| | | server2 = openReplicationSession(baseDN, |
| | | server2ID, 100, getReplServerPort(replServer1ID), 1000); |
| | | server2ID, 100, getReplServerPort(replServer1ID), 10000); |
| | | } |
| | | |
| | | // Creates config to synchronize suffix |
| | |
| | | |
| | | // Connects lDAP2 to replServer2 |
| | | broker2 = openReplicationSession(baseDN, |
| | | server2ID, 100, getReplServerPort(replServer2ID), 1000); |
| | | server2ID, 100, getReplServerPort(replServer2ID), 10000); |
| | | |
| | | // Connects lDAP3 to replServer2 |
| | | broker3 = openReplicationSession(baseDN, |
| | | server3ID, 100, getReplServerPort(replServer2ID), 1000); |
| | | server3ID, 100, getReplServerPort(replServer2ID), 10000); |
| | | |
| | | // Check that the list of connected LDAP servers is correct in each replication servers |
| | | Assertions.assertThat(getConnectedDSServerIds(replServer1)).containsExactly(server1ID); |
| | |
| | | Assertions.assertThat(getConnectedDSServerIds(replServer2)).containsExactly(server2ID); |
| | | |
| | | broker3 = openReplicationSession(baseDN, |
| | | server3ID, 100, getReplServerPort(replServer2ID), 1000); |
| | | server3ID, 100, getReplServerPort(replServer2ID), 10000); |
| | | broker2.stop(); |
| | | Thread.sleep(1000); |
| | | Assertions.assertThat(getConnectedDSServerIds(replServer2)).containsExactly(server3ID); |
| | |
| | | { |
| | | log(testCase + " Will connect server 2 to " + replServer2ID); |
| | | server2 = openReplicationSession(baseDN, |
| | | server2ID, 100, getReplServerPort(replServer2ID), 1000); |
| | | server2ID, 100, getReplServerPort(replServer2ID), 10000); |
| | | } |
| | | |
| | | // Launch in S1 the task that will initialize S2 |
| | |
| | | log(testCase + " Will connect server 2 to " + replServer2ID); |
| | | server2 = openReplicationSession(baseDN, |
| | | server2ID, 100, getReplServerPort(replServer2ID), |
| | | 1000, replServer1.getGenerationId(baseDN)); |
| | | 10000, replServer1.getGenerationId(baseDN)); |
| | | } |
| | | |
| | | // Connect a broker acting as server 3 to Repl Server 3 |
| | |
| | | log(testCase + " Will connect server 3 to " + replServer3ID); |
| | | server3 = openReplicationSession(baseDN, |
| | | server3ID, 100, getReplServerPort(replServer3ID), |
| | | 1000, replServer1.getGenerationId(baseDN)); |
| | | 10000, replServer1.getGenerationId(baseDN)); |
| | | } |
| | | |
| | | // S3 sends init request |
| | |
| | | if (server2 == null) |
| | | { |
| | | server2 = openReplicationSession(baseDN, |
| | | server2ID, 100, getReplServerPort(replServer1ID), 1000); |
| | | server2ID, 100, getReplServerPort(replServer1ID), 10000); |
| | | } |
| | | |
| | | // Creates config to synchronize suffix |
| New file |
| | |
| | | /* |
| | | * The contents of this file are subject to the terms of the Common Development and |
| | | * Distribution License (the License). You may not use this file except in compliance with the |
| | | * License. |
| | | * |
| | | * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the |
| | | * specific language governing permission and limitations under the License. |
| | | * |
| | | * When distributing Covered Software, include this CDDL Header Notice in each file and include |
| | | * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.replication.server; |
| | | |
| | | import static org.opends.server.TestCaseUtils.*; |
| | | import static org.opends.server.util.CollectionUtils.*; |
| | | import static org.testng.Assert.*; |
| | | |
| | | import java.net.ServerSocket; |
| | | import java.net.Socket; |
| | | import java.util.TreeSet; |
| | | |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.opends.server.TestCaseUtils; |
| | | import org.opends.server.replication.ReplicationTestCase; |
| | | import org.opends.server.replication.protocol.ReplServerStartMsg; |
| | | import org.opends.server.replication.protocol.ReplSessionSecurity; |
| | | import org.opends.server.replication.protocol.Session; |
| | | import org.opends.server.replication.protocol.StopMsg; |
| | | import org.testng.annotations.Test; |
| | | |
| | | /** |
| | | * Deterministic reproducer for the generation id rollback race fixed in the |
| | | * same change (issue #735): an outgoing RS to RS handshake that is aborted |
| | | * (e.g. rejected by the peer on a simultaneous cross-connect) must not roll |
| | | * the domain generation id back to the value it had when the handshake |
| | | * started, wiping a value that was legitimately adopted in the meantime. |
| | | * <p> |
| | | * The test plays the remote peer itself, so it fully controls the interleaving: |
| | | * the connecting RS snapshots the domain generation id before sending its start |
| | | * message, the test then changes the generation id while the RS is blocked |
| | | * waiting for the answer, and only then rejects the handshake with a StopMsg. |
| | | * The generation id advertised on the next connection attempt is sent strictly |
| | | * after the abort completed on the same connector thread, so it exposes a |
| | | * rollback deterministically, without any timing assumptions. |
| | | */ |
| | | @SuppressWarnings("javadoc") |
| | | public class HandshakeAbortGenerationIdTest extends ReplicationTestCase |
| | | { |
| | | private static final long ADOPTED_GEN_ID = 4801; |
| | | private static final int SOCKET_TIMEOUT_MS = 30000; |
| | | |
| | | @Test |
| | | public void abortedConnectMustNotRollBackConcurrentlyAdoptedGenId() throws Exception |
| | | { |
| | | final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING); |
| | | ReplicationServer replicationServer = null; |
| | | try (ServerSocket fakePeer = TestCaseUtils.bindFreePort()) |
| | | { |
| | | fakePeer.setSoTimeout(SOCKET_TIMEOUT_MS); |
| | | |
| | | final int rsPort = TestCaseUtils.findFreePort(); |
| | | replicationServer = new ReplicationServer(new ReplServerFakeConfiguration( |
| | | rsPort, "handshakeAbortGenIdTestDb", 0, 811, 0, 100, |
| | | newTreeSet("127.0.0.1:" + fakePeer.getLocalPort()))); |
| | | // Creating the domain makes the RS connect thread dial the fake peer. |
| | | final ReplicationServerDomain domain = |
| | | replicationServer.getReplicationServerDomain(baseDN, true); |
| | | |
| | | final ReplSessionSecurity security = getReplSessionSecurity(); |
| | | try (Session session = accept(fakePeer, security)) |
| | | { |
| | | // The RS is now inside connect(): it snapshotted the domain generation |
| | | // id (-1) before sending its start message. |
| | | final ReplServerStartMsg startMsg = |
| | | waitForSpecificMsg(session, ReplServerStartMsg.class); |
| | | assertEquals(startMsg.getGenerationId(), -1); |
| | | |
| | | // While connect() is blocked waiting for our answer, the domain adopts |
| | | // a generation id, as happens when gossip arrives from another peer RS. |
| | | domain.changeGenerationId(ADOPTED_GEN_ID); |
| | | |
| | | // Reject the handshake as a peer does on a simultaneous cross-connect. |
| | | session.publish(new StopMsg()); |
| | | } |
| | | |
| | | // The second connection attempt is made by the same connector thread |
| | | // strictly after abortStart() returned, so the generation id it |
| | | // advertises deterministically shows whether the abort rolled back the |
| | | // concurrently adopted value. |
| | | try (Session session = accept(fakePeer, security)) |
| | | { |
| | | final ReplServerStartMsg startMsg = |
| | | waitForSpecificMsg(session, ReplServerStartMsg.class); |
| | | assertEquals(startMsg.getGenerationId(), ADOPTED_GEN_ID, |
| | | "the aborted handshake rolled back a generation id it did not set"); |
| | | session.publish(new StopMsg()); |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | removeQuietly(replicationServer); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * The guards on the generation id primitives introduced for the abort |
| | | * rollback: a compare-and-set never overwrites a value it did not expect, |
| | | * and a rollback never clears a generation id that has been saved to the |
| | | * changelog. |
| | | */ |
| | | @Test |
| | | public void generationIdPrimitivesHonorGuards() throws Exception |
| | | { |
| | | final DN baseDN = DN.valueOf(TEST_ROOT_DN_STRING); |
| | | ReplicationServer replicationServer = null; |
| | | try |
| | | { |
| | | replicationServer = new ReplicationServer(new ReplServerFakeConfiguration( |
| | | TestCaseUtils.findFreePort(), "handshakeAbortGenIdPrimitivesDb", 0, |
| | | 812, 0, 100, new TreeSet<String>())); |
| | | final ReplicationServerDomain domain = |
| | | replicationServer.getReplicationServerDomain(baseDN, true); |
| | | |
| | | assertFalse(domain.changeGenerationIdIfUnchanged(123, 456), |
| | | "CAS with a stale expected value must not fire"); |
| | | assertEquals(domain.getGenerationId(), -1); |
| | | |
| | | assertTrue(domain.changeGenerationIdIfUnchanged(-1, ADOPTED_GEN_ID)); |
| | | assertEquals(domain.getGenerationId(), ADOPTED_GEN_ID); |
| | | |
| | | domain.rollbackGenerationIdIfUnchanged(999, -1); |
| | | assertEquals(domain.getGenerationId(), ADOPTED_GEN_ID, |
| | | "rollback with a stale expected value must not fire"); |
| | | |
| | | domain.rollbackGenerationIdIfUnchanged(ADOPTED_GEN_ID, -1); |
| | | assertEquals(domain.getGenerationId(), -1, |
| | | "rollback must fire when the value is unchanged and unsaved"); |
| | | |
| | | domain.initGenerationID(ADOPTED_GEN_ID); |
| | | domain.rollbackGenerationIdIfUnchanged(ADOPTED_GEN_ID, -1); |
| | | assertEquals(domain.getGenerationId(), ADOPTED_GEN_ID, |
| | | "rollback must never clear a saved generation id"); |
| | | } |
| | | finally |
| | | { |
| | | removeQuietly(replicationServer); |
| | | } |
| | | } |
| | | |
| | | /** Teardown must never mask the primary assertion failure. */ |
| | | private void removeQuietly(ReplicationServer replicationServer) |
| | | { |
| | | try |
| | | { |
| | | remove(replicationServer); |
| | | } |
| | | catch (Exception ignored) |
| | | { |
| | | } |
| | | } |
| | | |
| | | private Session accept(ServerSocket listenSocket, ReplSessionSecurity security) |
| | | throws Exception |
| | | { |
| | | final Socket socket = listenSocket.accept(); |
| | | socket.setTcpNoDelay(true); |
| | | final Session session = security.createServerSession(socket, SOCKET_TIMEOUT_MS); |
| | | assertNotNull(session, "could not create a session with the connecting RS"); |
| | | return session; |
| | | } |
| | | } |
| | |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2024-2025 3A Systems, LLC. |
| | | * Copyright 2024-2026 3A Systems, LLC. |
| | | */ |
| | | package org.openidentityplatform.opendj; |
| | | |
| | | import org.forgerock.opendj.config.client.ManagementContext; |
| | | import org.forgerock.opendj.ldap.*; |
| | | import org.forgerock.opendj.ldap.controls.PersistentSearchChangeType; |
| | | import org.forgerock.opendj.ldap.controls.PersistentSearchRequestControl; |
| | | import org.forgerock.opendj.ldap.requests.Requests; |
| | | import org.forgerock.opendj.ldap.requests.SearchRequest; |
| | | import org.forgerock.opendj.ldap.responses.SearchResultEntry; |
| | | import org.forgerock.opendj.ldap.responses.SearchResultReference; |
| | | import org.forgerock.opendj.ldif.ConnectionEntryReader; |
| | | import org.forgerock.opendj.server.config.client.GlobalCfgClient; |
| | | import org.forgerock.opendj.server.config.meta.GlobalCfgDefn; |
| | | import org.opends.server.DirectoryServerTestCase; |
| | | import org.opends.server.TestCaseUtils; |
| | | |
| | | import org.opends.server.api.LocalBackend; |
| | | import org.opends.server.backends.MemoryBackend; |
| | | import org.opends.server.core.DirectoryServer; |
| | | import org.opends.server.types.AcceptRejectWarn; |
| | | import org.opends.server.types.Entry; |
| | | import org.testng.annotations.AfterClass; |
| | | import org.testng.annotations.BeforeClass; |
| | | import org.testng.annotations.Test; |
| | | |
| | | import java.util.HashMap; |
| | | import java.util.concurrent.BlockingQueue; |
| | | import java.util.concurrent.LinkedBlockingQueue; |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | | import static org.assertj.core.api.Assertions.assertThat; |
| | | import static org.assertj.core.api.Assertions.fail; |
| | | |
| | | @Test(sequential = true) |
| | | public class AliasTestCase extends DirectoryServerTestCase { |
| | | /** Backend holding a naming context other than o=test, to check aliases crossing a backend boundary. */ |
| | | static final String OTHER_BACKEND_ID = "test2"; |
| | | |
| | | Connection connection; |
| | | |
| | | @BeforeClass |
| | | public void startServer() throws Exception { |
| | | TestCaseUtils.startServer(); |
| | | TestCaseUtils.initializeTestBackend(true); |
| | | TestCaseUtils.initializeMemoryBackend(OTHER_BACKEND_ID, "o=test2", true); |
| | | |
| | | TestCaseUtils.addEntries( |
| | | "dn: o=MyCompany, o=test", |
| | |
| | | "objectClass: extensibleObject", |
| | | "aliasedObjectName: uid=janedoe,ou=students,o=test", |
| | | "uid: janedoe", |
| | | "", |
| | | |
| | | //an alias pointing into the o=test2 backend, and one pointing nowhere at all |
| | | "dn: ou=people,o=test2", |
| | | "objectClass: top", |
| | | "objectClass: organizationalUnit", |
| | | "ou: people", |
| | | "", |
| | | "dn: cn=Jane Roe,ou=people,o=test2", |
| | | "cn: Jane Roe", |
| | | "sn: Roe", |
| | | "objectclass: person", |
| | | "", |
| | | "dn: ou=external,o=test", |
| | | "objectClass: alias", |
| | | "objectClass: top", |
| | | "objectClass: extensibleObject", |
| | | "ou: external", |
| | | "aliasedObjectName: ou=people,o=test2", |
| | | "", |
| | | "dn: ou=nowhere,o=test", |
| | | "objectClass: alias", |
| | | "objectClass: top", |
| | | "objectClass: extensibleObject", |
| | | "ou: nowhere", |
| | | "aliasedObjectName: ou=people,o=missing", |
| | | "" |
| | | ); |
| | | |
| | |
| | | assertThat(connection.isValid()).isTrue(); |
| | | } |
| | | |
| | | @AfterClass |
| | | public void stopServer() throws Exception { |
| | | if (connection != null) { |
| | | connection.close(); |
| | | } |
| | | final MemoryBackend otherBackend = (MemoryBackend) TestCaseUtils.getServerContext() |
| | | .getBackendConfigManager().getLocalBackendById(OTHER_BACKEND_ID); |
| | | if (otherBackend != null) { |
| | | otherBackend.clearMemoryBackend(); |
| | | otherBackend.finalizeBackend(); |
| | | TestCaseUtils.getServerContext().getBackendConfigManager().deregisterLocalBackend(otherBackend); |
| | | } |
| | | } |
| | | |
| | | public HashMap<String,SearchResultEntry> search(SearchScope scope,DereferenceAliasesPolicy policy) throws SearchResultReferenceIOException, LdapException { |
| | | return search("ou=Area1,o=test", scope, policy); |
| | | } |
| | |
| | | assertThat(res.containsKey("uid=janedoe,ou=employees,o=test")).isFalse(); |
| | | } |
| | | |
| | | // The alias target does not have to live in the backend holding the alias: |
| | | // the search must be handed over to the backend holding the target. |
| | | @Test |
| | | public void test_cross_backend_alias_base_always() throws LdapException, SearchResultReferenceIOException { |
| | | HashMap<String, SearchResultEntry> res = search("ou=external,o=test", SearchScope.BASE_OBJECT, DereferenceAliasesPolicy.ALWAYS); |
| | | |
| | | assertThat(res.containsKey("ou=external,o=test")).isFalse(); |
| | | assertThat(res.containsKey("ou=people,o=test2")).isTrue(); |
| | | assertThat(res.containsKey("cn=Jane Roe,ou=people,o=test2")).isFalse(); |
| | | } |
| | | |
| | | @Test |
| | | public void test_cross_backend_alias_sub_always() throws LdapException, SearchResultReferenceIOException { |
| | | HashMap<String, SearchResultEntry> res = search("ou=external,o=test", SearchScope.WHOLE_SUBTREE, DereferenceAliasesPolicy.ALWAYS); |
| | | |
| | | assertThat(res.containsKey("ou=external,o=test")).isFalse(); |
| | | assertThat(res.containsKey("ou=people,o=test2")).isTrue(); |
| | | assertThat(res.containsKey("cn=Jane Roe,ou=people,o=test2")).isTrue(); |
| | | } |
| | | |
| | | // No backend at all holds the alias target: this is a base DN that does not exist. |
| | | @Test |
| | | public void test_alias_target_without_backend() throws SearchResultReferenceIOException { |
| | | try { |
| | | search("ou=nowhere,o=test", SearchScope.BASE_OBJECT, DereferenceAliasesPolicy.ALWAYS); |
| | | fail("dereferencing an alias whose target has no backend must fail"); |
| | | } catch (LdapException e) { |
| | | assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.NO_SUCH_OBJECT); |
| | | } |
| | | } |
| | | |
| | | // An alias whose target lies outside the search scope: the target is only |
| | | // reachable by dereferencing, so it must be returned by the search. |
| | | @Test |
| | | public void test_deref_target_outside_scope() throws Exception { |
| | | TestCaseUtils.addEntries( |
| | | "dn: ou=pointers,o=test", |
| | | "objectClass: top", |
| | | "objectClass: organizationalUnit", |
| | | "ou: pointers", |
| | | "", |
| | | "dn: cn=ptr,ou=pointers,o=test", |
| | | "objectClass: alias", |
| | | "objectClass: top", |
| | | "objectClass: extensibleObject", |
| | | "cn: ptr", |
| | | "aliasedObjectName: cn=John Doe,o=MyCompany,o=test", |
| | | "" |
| | | ); |
| | | HashMap<String, SearchResultEntry> res = |
| | | search("ou=pointers,o=test", SearchScope.WHOLE_SUBTREE, DereferenceAliasesPolicy.ALWAYS); |
| | | |
| | | assertThat(res.containsKey("cn=John Doe,o=MyCompany,o=test")).isTrue(); |
| | | } |
| | | |
| | | // An alias pointing at an entry which does not exist is skipped, and the entries next to it are |
| | | // still returned: aliases are not checked when they are written, so a dangling one is expected. |
| | | @Test |
| | | public void test_dangling_alias() throws Exception { |
| | | TestCaseUtils.addEntries( |
| | | "dn: ou=dangling,o=test", |
| | | "objectClass: top", |
| | | "objectClass: organizationalUnit", |
| | | "ou: dangling", |
| | | "", |
| | | "dn: cn=broken,ou=dangling,o=test", |
| | | "objectClass: alias", |
| | | "objectClass: top", |
| | | "objectClass: extensibleObject", |
| | | "cn: broken", |
| | | "aliasedObjectName: cn=does-not-exist,ou=dangling,o=test", |
| | | "", |
| | | "dn: cn=intact,ou=dangling,o=test", |
| | | "cn: intact", |
| | | "sn: intact", |
| | | "objectClass: person", |
| | | "" |
| | | ); |
| | | HashMap<String, SearchResultEntry> res = |
| | | search("ou=dangling,o=test", SearchScope.WHOLE_SUBTREE, DereferenceAliasesPolicy.ALWAYS); |
| | | |
| | | assertThat(res.containsKey("cn=does-not-exist,ou=dangling,o=test")).isFalse(); |
| | | assertThat(res.containsKey("ou=dangling,o=test")).isTrue(); |
| | | assertThat(res.containsKey("cn=intact,ou=dangling,o=test")).isTrue(); |
| | | } |
| | | |
| | | // A persistent search reports every change it is notified of. Dereferencing aliases must not |
| | | // make it drop a change because the entry was notified before. |
| | | @Test |
| | | public void test_persistent_search_reports_repeated_changes() throws Exception { |
| | | TestCaseUtils.addEntries( |
| | | "dn: ou=psearch,o=test", |
| | | "objectClass: top", |
| | | "objectClass: organizationalUnit", |
| | | "ou: psearch", |
| | | "", |
| | | "dn: cn=changing,ou=psearch,o=test", |
| | | "cn: changing", |
| | | "sn: changing", |
| | | "objectClass: person", |
| | | "" |
| | | ); |
| | | |
| | | final BlockingQueue<String> notified = new LinkedBlockingQueue<>(); |
| | | final SearchRequest request = |
| | | Requests.newSearchRequest("ou=psearch,o=test", SearchScope.WHOLE_SUBTREE, "(objectclass=*)") |
| | | .setDereferenceAliasesPolicy(DereferenceAliasesPolicy.ALWAYS) |
| | | .addControl(PersistentSearchRequestControl.newControl( |
| | | true, true, false, PersistentSearchChangeType.MODIFY)); |
| | | |
| | | final LDAPConnectionFactory factory = |
| | | new LDAPConnectionFactory("localhost", TestCaseUtils.getServerLdapPort()); |
| | | try (Connection psearch = factory.getConnection()) { |
| | | psearch.bind("cn=Directory Manager", "password".toCharArray()); |
| | | psearch.searchAsync(request, new SearchResultHandler() { |
| | | @Override |
| | | public boolean handleEntry(SearchResultEntry entry) { |
| | | notified.add(entry.getName().toString()); |
| | | return true; |
| | | } |
| | | |
| | | @Override |
| | | public boolean handleReference(SearchResultReference reference) { |
| | | return true; |
| | | } |
| | | }); |
| | | |
| | | // searchAsync returns before the server has registered the persistent search, so wait |
| | | // until the backend reports it; otherwise the first modification below can be notified |
| | | // before the search is listening and be missed. |
| | | final LocalBackend<?> backend = TestCaseUtils.getServerContext() |
| | | .getBackendConfigManager().getLocalBackendById(TestCaseUtils.TEST_BACKEND_ID); |
| | | for (int i = 0; backend.getPersistentSearches().isEmpty() && i < 500; i++) { |
| | | Thread.sleep(10); |
| | | } |
| | | assertThat(backend.getPersistentSearches()).isNotEmpty(); |
| | | |
| | | // The same entry is modified repeatedly: each change must reach the persistent search. |
| | | for (int i = 1; i <= 3; i++) { |
| | | connection.modify(Requests.newModifyRequest("cn=changing,ou=psearch,o=test") |
| | | .addModification(ModificationType.REPLACE, "description", "change " + i)); |
| | | assertThat(notified.poll(30, TimeUnit.SECONDS)) |
| | | .as("notification for change " + i) |
| | | .isEqualTo("cn=changing,ou=psearch,o=test"); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // An alias is dereferenced before its target is reached on its own: the target must still be |
| | | // returned, and exactly once. The original regression was order-sensitive, dropping the target |
| | | // when the alias reached it first, so this pins the alias-before-target order specifically. The |
| | | // MemoryBackend serving o=test walks entries in insertion order, so listing the alias before the |
| | | // target below guarantees the alias is visited first. |
| | | @Test |
| | | public void test_deref_alias_before_target_returns_target_once() throws Exception { |
| | | TestCaseUtils.addEntries( |
| | | "dn: ou=order,o=test", |
| | | "objectClass: top", |
| | | "objectClass: organizationalUnit", |
| | | "ou: order", |
| | | "", |
| | | "dn: cn=aaa-alias,ou=order,o=test", |
| | | "objectClass: alias", |
| | | "objectClass: top", |
| | | "objectClass: extensibleObject", |
| | | "cn: aaa-alias", |
| | | "aliasedObjectName: cn=zzz-target,ou=order,o=test", |
| | | "", |
| | | "dn: cn=zzz-target,ou=order,o=test", |
| | | "cn: zzz-target", |
| | | "sn: target", |
| | | "objectClass: person", |
| | | "" |
| | | ); |
| | | // search() asserts every returned DN is unique, so a duplicated target would fail this call. |
| | | HashMap<String, SearchResultEntry> res = |
| | | search("ou=order,o=test", SearchScope.WHOLE_SUBTREE, DereferenceAliasesPolicy.ALWAYS); |
| | | |
| | | // The target is returned, reached through the alias visited before it ... |
| | | assertThat(res.containsKey("cn=zzz-target,ou=order,o=test")).isTrue(); |
| | | // ... and the alias entry itself is replaced by its target, never returned as-is. |
| | | assertThat(res.containsKey("cn=aaa-alias,ou=order,o=test")).isFalse(); |
| | | } |
| | | |
| | | // An alias whose aliasedObjectName is not a valid DN cannot be dereferenced. Reading it throws a |
| | | // DirectoryException, which must be logged and the alias skipped, not turned into a failed search. |
| | | @Test |
| | | public void test_alias_with_malformed_target_is_skipped() throws Exception { |
| | | // A malformed DN can only be stored while syntax enforcement is relaxed; the default REJECT |
| | | // policy refuses the add outright. |
| | | final AcceptRejectWarn previous = DirectoryServer.getCoreConfigManager().getSyntaxEnforcementPolicy(); |
| | | setSyntaxEnforcementPolicy(GlobalCfgDefn.InvalidAttributeSyntaxBehavior.WARN); |
| | | try { |
| | | TestCaseUtils.addEntries( |
| | | "dn: ou=malformed,o=test", |
| | | "objectClass: top", |
| | | "objectClass: organizationalUnit", |
| | | "ou: malformed", |
| | | "", |
| | | "dn: cn=badptr,ou=malformed,o=test", |
| | | "objectClass: alias", |
| | | "objectClass: top", |
| | | "objectClass: extensibleObject", |
| | | "cn: badptr", |
| | | "aliasedObjectName: not a valid dn", |
| | | "", |
| | | "dn: cn=neighbour,ou=malformed,o=test", |
| | | "cn: neighbour", |
| | | "sn: neighbour", |
| | | "objectClass: person", |
| | | "" |
| | | ); |
| | | } finally { |
| | | setSyntaxEnforcementPolicy(asSyntaxBehavior(previous)); |
| | | } |
| | | |
| | | // The whole search still succeeds: the broken alias is skipped, its neighbours returned. |
| | | HashMap<String, SearchResultEntry> res = |
| | | search("ou=malformed,o=test", SearchScope.WHOLE_SUBTREE, DereferenceAliasesPolicy.ALWAYS); |
| | | |
| | | assertThat(res.containsKey("cn=badptr,ou=malformed,o=test")).isFalse(); |
| | | assertThat(res.containsKey("ou=malformed,o=test")).isTrue(); |
| | | assertThat(res.containsKey("cn=neighbour,ou=malformed,o=test")).isTrue(); |
| | | } |
| | | |
| | | private static void setSyntaxEnforcementPolicy(GlobalCfgDefn.InvalidAttributeSyntaxBehavior value) throws Exception { |
| | | try (ManagementContext conf = TestCaseUtils.getServer().getConfiguration()) { |
| | | GlobalCfgClient globalCfg = conf.getRootConfiguration().getGlobalConfiguration(); |
| | | globalCfg.setInvalidAttributeSyntaxBehavior(value); |
| | | globalCfg.commit(); |
| | | } |
| | | } |
| | | |
| | | private static GlobalCfgDefn.InvalidAttributeSyntaxBehavior asSyntaxBehavior(AcceptRejectWarn policy) { |
| | | switch (policy) { |
| | | case ACCEPT: |
| | | return GlobalCfgDefn.InvalidAttributeSyntaxBehavior.ACCEPT; |
| | | case WARN: |
| | | return GlobalCfgDefn.InvalidAttributeSyntaxBehavior.WARN; |
| | | case REJECT: |
| | | default: |
| | | return GlobalCfgDefn.InvalidAttributeSyntaxBehavior.REJECT; |
| | | } |
| | | } |
| | | |
| | | @Test(expectedExceptions = LdapException.class) |
| | | public void test_stackoverflow() throws Exception { |
| | | |