[#738] Fix dereferencing an alias that points into another backend (#741)
5 files modified
1 files added
| | |
| | | * |
| | | * 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); |
| | | } |
| | | |
| | | // 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 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) { |
| 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(); |
| | |
| | | * 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.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", |
| | |
| | | "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); |
| | | } |
| | | } |
| | | |
| | | @Test(expectedExceptions = LdapException.class) |
| | | public void test_stackoverflow() throws Exception { |
| | | |