Skip to content

Commit c549b64

Browse files
authored
[#738] Fix dereferencing an alias that points into another backend (#741)
1 parent ba2b247 commit c549b64

6 files changed

Lines changed: 204 additions & 12 deletions

File tree

opendj-server-legacy/src/main/java/org/opends/server/backends/MemoryBackend.java

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*
1414
* Copyright 2006-2008 Sun Microsystems, Inc.
1515
* Portions Copyright 2014-2016 ForgeRock AS.
16+
* Portions Copyrighted 2026 3A Systems, LLC.
1617
*/
1718
package org.opends.server.backends;
1819

@@ -494,9 +495,10 @@ public synchronized void search(SearchOperation searchOperation)
494495
SearchScope scope = searchOperation.getScope();
495496
SearchFilter filter = searchOperation.getFilter();
496497

497-
// Make sure the base entry exists if it's supposed to be in this backend.
498+
// Make sure the base entry exists. A base DN this backend does not hold has no entry
499+
// either, so it must be reported as missing rather than searched for.
498500
Entry baseEntry = entryMap.get(baseDN);
499-
if (baseEntry == null && handlesEntry(baseDN))
501+
if (baseEntry == null)
500502
{
501503
DN matchedDN = serverContext.getBackendConfigManager().getParentDNInSuffix(baseDN);
502504
while (matchedDN != null)
@@ -515,10 +517,7 @@ public synchronized void search(SearchOperation searchOperation)
515517
ResultCode.NO_SUCH_OBJECT, message, matchedDN, null);
516518
}
517519

518-
if (baseEntry != null)
519-
{
520-
baseEntry = baseEntry.duplicate(true);
521-
}
520+
baseEntry = baseEntry.duplicate(true);
522521

523522
// If it's a base-level search, then just get that entry and return it if it
524523
// matches the filter.

opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/BackendImpl.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
*
1414
* Copyright 2007-2010 Sun Microsystems, Inc.
1515
* Portions Copyright 2013-2016 ForgeRock AS.
16-
* Portions Copyright 2025 3A Systems, LLC
16+
* Portions Copyright 2025-2026 3A Systems, LLC
1717
*/
1818
package org.opends.server.backends.pluggable;
1919

@@ -122,13 +122,29 @@ public abstract class BackendImpl<C extends PluggableBackendCfg> extends LocalBa
122122
* @return <code>EntryContainer</code> where <code>entryDN</code> resides
123123
*/
124124
private EntryContainer accessBegin(Operation operation, DN entryDN) throws DirectoryException
125+
{
126+
return accessBegin(operation, entryDN, ResultCode.UNDEFINED);
127+
}
128+
129+
/**
130+
* Begin a Backend API method that accesses the {@link EntryContainer} for <code>entryDN</code>
131+
* and returns it.
132+
* @param operation requesting the storage
133+
* @param entryDN the target DN for the operation
134+
* @param noEntryContainerResultCode the result code to report when this backend holds no
135+
* entry container for <code>entryDN</code>
136+
* @return <code>EntryContainer</code> where <code>entryDN</code> resides
137+
*/
138+
private EntryContainer accessBegin(Operation operation, DN entryDN, ResultCode noEntryContainerResultCode)
139+
throws DirectoryException
125140
{
126141
checkRootContainerInitialized();
127142
rootContainer.checkForEnoughResources(operation);
128143
EntryContainer ec = rootContainer.getEntryContainer(entryDN);
129144
if (ec == null)
130145
{
131-
throw new DirectoryException(ResultCode.UNDEFINED, ERR_BACKEND_ENTRY_DOESNT_EXIST.get(entryDN, getBackendID()));
146+
throw new DirectoryException(
147+
noEntryContainerResultCode, ERR_BACKEND_ENTRY_DOESNT_EXIST.get(entryDN, getBackendID()));
132148
}
133149
threadTotalCount.getAndIncrement();
134150
return ec;
@@ -557,7 +573,9 @@ public void renameEntry(DN currentDN, Entry entry, ModifyDNOperation modifyDNOpe
557573
@Override
558574
public void search(SearchOperation searchOperation) throws DirectoryException, CanceledOperationException
559575
{
560-
EntryContainer ec = accessBegin(searchOperation, searchOperation.getBaseDN());
576+
// a base DN held by no entry container of this backend does not exist as far as a client
577+
// is concerned: report it as such instead of the UNDEFINED result code used internally.
578+
EntryContainer ec = accessBegin(searchOperation, searchOperation.getBaseDN(), ResultCode.NO_SUCH_OBJECT);
561579

562580
ec.sharedLock.lock();
563581

opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendSearchOperation.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
*
1414
* Copyright 2008-2010 Sun Microsystems, Inc.
1515
* Portions Copyright 2011-2016 ForgeRock AS.
16-
* Portions Copyright 2024-2025 3A Systems, LLC.
16+
* Portions Copyright 2024-2026 3A Systems, LLC.
1717
*/
1818
package org.opends.server.workflowelement.localbackend;
1919

@@ -216,6 +216,10 @@ private void processSearch(AtomicBoolean executePostOpPlugins) throws CanceledOp
216216
if(!dereferencingDNs.contains(aliasedDn)) { //detect recursive search
217217
dereferencingDNs.add(aliasedDn);
218218
setBaseDN(aliasedDn);
219+
// the alias may point into another backend: a null backend is reported
220+
// as NO_SUCH_OBJECT by the recursive call
221+
backend = DirectoryServer.getInstance().getServerContext()
222+
.getBackendConfigManager().findLocalBackendForEntry(aliasedDn);
219223
try {
220224
processSearch(executePostOpPlugins);
221225
} catch (StackOverflowError error) {
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* The contents of this file are subject to the terms of the Common Development and
3+
* Distribution License (the License). You may not use this file except in compliance with the
4+
* License.
5+
*
6+
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7+
* specific language governing permission and limitations under the License.
8+
*
9+
* When distributing Covered Software, include this CDDL Header Notice in each file and include
10+
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11+
* Header, with the fields enclosed by brackets [] replaced by your own identifying
12+
* information: "Portions copyright [year] [name of copyright owner]".
13+
*
14+
* Copyright 2026 3A Systems, LLC.
15+
*/
16+
package org.opends.server.backends;
17+
18+
import static org.opends.server.protocols.internal.InternalClientConnection.getRootConnection;
19+
import static org.opends.server.protocols.internal.Requests.newSearchRequest;
20+
import static org.testng.Assert.assertEquals;
21+
import static org.testng.Assert.fail;
22+
23+
import org.forgerock.opendj.ldap.DN;
24+
import org.forgerock.opendj.ldap.ResultCode;
25+
import org.forgerock.opendj.ldap.SearchScope;
26+
import org.opends.server.TestCaseUtils;
27+
import org.opends.server.protocols.internal.InternalSearchOperation;
28+
import org.opends.server.protocols.internal.SearchRequest;
29+
import org.opends.server.types.DirectoryException;
30+
import org.opends.server.workflowelement.localbackend.LocalBackendSearchOperation;
31+
import org.testng.annotations.BeforeClass;
32+
import org.testng.annotations.Test;
33+
34+
/** Test cases for the memory backend. */
35+
@SuppressWarnings("javadoc")
36+
public class MemoryBackendTestCase extends BackendTestCase
37+
{
38+
private MemoryBackend backend;
39+
40+
@BeforeClass
41+
public void startServer() throws Exception
42+
{
43+
TestCaseUtils.startServer();
44+
TestCaseUtils.initializeTestBackend(true);
45+
backend = (MemoryBackend) TestCaseUtils.getServerContext().getBackendConfigManager()
46+
.getLocalBackendById(TestCaseUtils.TEST_BACKEND_ID);
47+
}
48+
49+
/** A base DN this backend does not hold has no entry either. */
50+
@Test
51+
public void testSearchBaseDNNotHeldByBackend() throws Exception
52+
{
53+
assertSearchFailsWithNoSuchObject(DN.valueOf("o=not-held-by-this-backend"));
54+
}
55+
56+
/** A base DN this backend holds, but which has never been added. */
57+
@Test
58+
public void testSearchBaseDNWithoutEntry() throws Exception
59+
{
60+
assertSearchFailsWithNoSuchObject(DN.valueOf("ou=missing,o=test"));
61+
}
62+
63+
private void assertSearchFailsWithNoSuchObject(DN baseDN) throws Exception
64+
{
65+
final SearchRequest request = newSearchRequest(baseDN, SearchScope.BASE_OBJECT);
66+
final InternalSearchOperation search = new InternalSearchOperation(getRootConnection(), -1, -1, request);
67+
try
68+
{
69+
backend.search(new LocalBackendSearchOperation(search));
70+
fail("Searching base DN " + baseDN + " should have failed.");
71+
}
72+
catch (DirectoryException e)
73+
{
74+
assertEquals(e.getResultCode(), ResultCode.NO_SUCH_OBJECT);
75+
}
76+
}
77+
}

opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/PluggableBackendImplTestCase.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
* information: "Portions Copyright [year] [name of copyright owner]".
1313
*
1414
* Copyright 2015-2016 ForgeRock AS.
15-
* Portions Copyright 2023-2025 3A Systems, LLC.
15+
* Portions Copyright 2023-2026 3A Systems, LLC.
1616
*/
1717
package org.opends.server.backends.pluggable;
1818

@@ -645,6 +645,22 @@ public void testHasSubordinates() throws Exception
645645
"Leaf entry should not have any subordinates.");
646646
}
647647

648+
/** A base DN held by no entry container of this backend has no entry either. */
649+
@Test
650+
public void testSearchBaseDNNotHeldByBackend() throws Exception
651+
{
652+
try
653+
{
654+
backend.search(createSearchOperation(
655+
DN.valueOf("dc=a"), SearchScope.BASE_OBJECT, "(objectClass=*)", new ArrayList<Entry>()));
656+
fail("Searching a base DN not held by this backend should have failed.");
657+
}
658+
catch (DirectoryException e)
659+
{
660+
assertEquals(e.getResultCode(), ResultCode.NO_SUCH_OBJECT);
661+
}
662+
}
663+
648664
private List<SearchResultEntry> runSearch(SearchRequest request, boolean useInternalConnection) throws Exception
649665
{
650666
InternalClientConnection conn = getRootConnection();

opendj-server-legacy/src/test/java/org/openidentityplatform/opendj/AliasTestCase.java

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* Header, with the fields enclosed by brackets [] replaced by your own identifying
1212
* information: "Portions Copyright [year] [name of copyright owner]".
1313
*
14-
* Copyright 2024-2025 3A Systems, LLC.
14+
* Copyright 2024-2026 3A Systems, LLC.
1515
*/
1616
package org.openidentityplatform.opendj;
1717

@@ -23,22 +23,29 @@
2323
import org.opends.server.DirectoryServerTestCase;
2424
import org.opends.server.TestCaseUtils;
2525

26+
import org.opends.server.backends.MemoryBackend;
2627
import org.opends.server.types.Entry;
28+
import org.testng.annotations.AfterClass;
2729
import org.testng.annotations.BeforeClass;
2830
import org.testng.annotations.Test;
2931

3032
import java.util.HashMap;
3133

3234
import static org.assertj.core.api.Assertions.assertThat;
35+
import static org.assertj.core.api.Assertions.fail;
3336

3437
@Test(sequential = true)
3538
public class AliasTestCase extends DirectoryServerTestCase {
39+
/** Backend holding a naming context other than o=test, to check aliases crossing a backend boundary. */
40+
static final String OTHER_BACKEND_ID = "test2";
41+
3642
Connection connection;
3743

3844
@BeforeClass
3945
public void startServer() throws Exception {
4046
TestCaseUtils.startServer();
4147
TestCaseUtils.initializeTestBackend(true);
48+
TestCaseUtils.initializeMemoryBackend(OTHER_BACKEND_ID, "o=test2", true);
4249

4350
TestCaseUtils.addEntries(
4451
"dn: o=MyCompany, o=test",
@@ -116,6 +123,32 @@ public void startServer() throws Exception {
116123
"objectClass: extensibleObject",
117124
"aliasedObjectName: uid=janedoe,ou=students,o=test",
118125
"uid: janedoe",
126+
"",
127+
128+
//an alias pointing into the o=test2 backend, and one pointing nowhere at all
129+
"dn: ou=people,o=test2",
130+
"objectClass: top",
131+
"objectClass: organizationalUnit",
132+
"ou: people",
133+
"",
134+
"dn: cn=Jane Roe,ou=people,o=test2",
135+
"cn: Jane Roe",
136+
"sn: Roe",
137+
"objectclass: person",
138+
"",
139+
"dn: ou=external,o=test",
140+
"objectClass: alias",
141+
"objectClass: top",
142+
"objectClass: extensibleObject",
143+
"ou: external",
144+
"aliasedObjectName: ou=people,o=test2",
145+
"",
146+
"dn: ou=nowhere,o=test",
147+
"objectClass: alias",
148+
"objectClass: top",
149+
"objectClass: extensibleObject",
150+
"ou: nowhere",
151+
"aliasedObjectName: ou=people,o=missing",
119152
""
120153
);
121154

@@ -125,6 +158,20 @@ public void startServer() throws Exception {
125158
assertThat(connection.isValid()).isTrue();
126159
}
127160

161+
@AfterClass
162+
public void stopServer() throws Exception {
163+
if (connection != null) {
164+
connection.close();
165+
}
166+
final MemoryBackend otherBackend = (MemoryBackend) TestCaseUtils.getServerContext()
167+
.getBackendConfigManager().getLocalBackendById(OTHER_BACKEND_ID);
168+
if (otherBackend != null) {
169+
otherBackend.clearMemoryBackend();
170+
otherBackend.finalizeBackend();
171+
TestCaseUtils.getServerContext().getBackendConfigManager().deregisterLocalBackend(otherBackend);
172+
}
173+
}
174+
128175
public HashMap<String,SearchResultEntry> search(SearchScope scope,DereferenceAliasesPolicy policy) throws SearchResultReferenceIOException, LdapException {
129176
return search("ou=Area1,o=test", scope, policy);
130177
}
@@ -386,6 +433,37 @@ public void test_alias_recursive_loop() throws LdapException, SearchResultRefere
386433
assertThat(res.containsKey("uid=janedoe,ou=employees,o=test")).isFalse();
387434
}
388435

436+
// The alias target does not have to live in the backend holding the alias:
437+
// the search must be handed over to the backend holding the target.
438+
@Test
439+
public void test_cross_backend_alias_base_always() throws LdapException, SearchResultReferenceIOException {
440+
HashMap<String, SearchResultEntry> res = search("ou=external,o=test", SearchScope.BASE_OBJECT, DereferenceAliasesPolicy.ALWAYS);
441+
442+
assertThat(res.containsKey("ou=external,o=test")).isFalse();
443+
assertThat(res.containsKey("ou=people,o=test2")).isTrue();
444+
assertThat(res.containsKey("cn=Jane Roe,ou=people,o=test2")).isFalse();
445+
}
446+
447+
@Test
448+
public void test_cross_backend_alias_sub_always() throws LdapException, SearchResultReferenceIOException {
449+
HashMap<String, SearchResultEntry> res = search("ou=external,o=test", SearchScope.WHOLE_SUBTREE, DereferenceAliasesPolicy.ALWAYS);
450+
451+
assertThat(res.containsKey("ou=external,o=test")).isFalse();
452+
assertThat(res.containsKey("ou=people,o=test2")).isTrue();
453+
assertThat(res.containsKey("cn=Jane Roe,ou=people,o=test2")).isTrue();
454+
}
455+
456+
// No backend at all holds the alias target: this is a base DN that does not exist.
457+
@Test
458+
public void test_alias_target_without_backend() throws SearchResultReferenceIOException {
459+
try {
460+
search("ou=nowhere,o=test", SearchScope.BASE_OBJECT, DereferenceAliasesPolicy.ALWAYS);
461+
fail("dereferencing an alias whose target has no backend must fail");
462+
} catch (LdapException e) {
463+
assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.NO_SUCH_OBJECT);
464+
}
465+
}
466+
389467
@Test(expectedExceptions = LdapException.class)
390468
public void test_stackoverflow() throws Exception {
391469

0 commit comments

Comments
 (0)