From 6d7a6bf1b8ec3801e8a60e51b203f0b8a6e160b4 Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Wed, 15 Jul 2026 15:54:27 +0300 Subject: [PATCH] [#737] Fix cn=changelog search failing when aliases are dereferenced ChangelogBackend.getEntry threw RuntimeException("Not implemented") instead of honouring the LocalBackend.getEntry contract of returning null for a missing entry. Since f7713150 the server reads the search base entry before handing the search to the backend whenever aliases are dereferenced, so every cn=changelog search with a deref policy of always, find, or subtree-scoped search failed with result code 80 before ChangelogBackend.search() was ever reached. This broke clients that dereference aliases by default, JNDI-based ones in particular. Return the base changelog entry by DN, and null for change records: those need the search filter and change number range to position their cursors, so they are only reachable through search(), and none of them is ever an alias. --- .../server/backends/ChangelogBackend.java | 14 +++- .../backends/ChangelogBackendTestCase.java | 80 +++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/ChangelogBackend.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/ChangelogBackend.java index 60c2f374f2..249f7b0fdb 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/backends/ChangelogBackend.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/ChangelogBackend.java @@ -297,6 +297,14 @@ public boolean isIndexed(final AttributeType attributeType, final IndexType inde return true; } + /** + * {@inheritDoc} + *

+ * 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 { @@ -305,7 +313,11 @@ 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 diff --git a/opendj-server-legacy/src/test/java/org/opends/server/backends/ChangelogBackendTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/backends/ChangelogBackendTestCase.java index 8e60c19099..d909e5b03c 100644 --- a/opendj-server-legacy/src/test/java/org/opends/server/backends/ChangelogBackendTestCase.java +++ b/opendj-server-legacy/src/test/java/org/opends/server/backends/ChangelogBackendTestCase.java @@ -12,6 +12,7 @@ * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2014-2016 ForgeRock AS. + * Portions Copyright 2026 3A Systems, LLC. */ package org.opends.server.backends; @@ -47,6 +48,7 @@ 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; @@ -783,6 +785,84 @@ public void searchWhenNoChangesShouldReturnRootEntryOnly() throws Exception 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 issue #737 + */ + @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 issue #737 + */ + @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 {