Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*
* Copyright 2006-2008 Sun Microsystems, Inc.
* Portions Copyright 2014-2016 ForgeRock AS.
* Portions Copyrighted 2026 3A Systems, LLC.
*/
package org.opends.server.backends;

Expand Down Expand Up @@ -494,9 +495,10 @@ public synchronized void search(SearchOperation searchOperation)
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)
Expand All @@ -515,10 +517,7 @@ public synchronized void search(SearchOperation searchOperation)
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*
* 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;

Expand Down Expand Up @@ -122,13 +122,29 @@ public abstract class BackendImpl<C extends PluggableBackendCfg> extends LocalBa
* @return <code>EntryContainer</code> where <code>entryDN</code> resides
*/
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;
Expand Down Expand Up @@ -557,7 +573,9 @@ public void renameEntry(DN currentDN, Entry entry, ModifyDNOperation modifyDNOpe
@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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*
* 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;

Expand Down Expand Up @@ -216,6 +216,10 @@ private void processSearch(AtomicBoolean executePostOpPlugins) throws CanceledOp
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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* 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;

Expand Down Expand Up @@ -645,6 +645,22 @@ public void testHasSubordinates() throws Exception
"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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* 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;

Expand All @@ -23,22 +23,29 @@
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;

import org.opends.server.backends.MemoryBackend;
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 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",
Expand Down Expand Up @@ -116,6 +123,32 @@ public void startServer() throws Exception {
"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",
""
);

Expand All @@ -125,6 +158,20 @@ public void startServer() throws Exception {
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);
}
Expand Down Expand Up @@ -386,6 +433,37 @@ public void test_alias_recursive_loop() throws LdapException, SearchResultRefere
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);
}
}

@Test(expectedExceptions = LdapException.class)
public void test_stackoverflow() throws Exception {

Expand Down
Loading