/* * 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.authorization.dseecompat; import static com.forgerock.opendj.ldap.CoreMessages.ERR_ATTR_SYNTAX_DN_TRAILING_ESCAPE; import static org.assertj.core.api.Assertions.*; import org.forgerock.i18n.LocalizedIllegalArgumentException; import org.forgerock.opendj.ldap.DN; import org.forgerock.opendj.ldap.ResultCode; import org.opends.server.DirectoryServerTestCase; import org.opends.server.TestCaseUtils; import org.opends.server.types.DirectoryException; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; /** * Verifies that {@link PatternDN}, which parses the ACI target and userdn patterns and the DN criteria of * the access log filtering, reads a pattern the same way {@link DN#valueOf(String)} reads a DN (issue #1153). */ @SuppressWarnings("javadoc") public class PatternDNTest extends DirectoryServerTestCase { @BeforeClass public void setUp() throws Exception { TestCaseUtils.startFakeServer(); } @AfterClass public void tearDown() throws DirectoryException { TestCaseUtils.shutdownFakeServer(); } /** Patterns without a wildcard: each one must match the DN that DN.valueOf() reads from the same string. */ @DataProvider public Object[][] patternsWithoutWildcard() { return new Object[][] { { "cn=a\\2Cb,dc=x" }, { "cn=\"a\\,b\",dc=x" }, // A hex pair inside quotes is decoded as it is outside quotes { "cn=\"a\\2Cb\",dc=x" }, { "cn=\"J\\C3\\B6rg\",dc=x" }, // Hex pairs that are still pending at the closing quote, or before an escaped character { "cn=\"ab\\2C\",dc=x" }, { "cn=\"\\C3\\B6\\,\",dc=x" }, // An empty value is followed by the next AVA or RDN, it does not swallow it { "cn=+sn=x,dc=y" }, { "cn=x+sn=,dc=y" }, { "cn=,dc=x" }, { "cn= ,dc=x" }, { "cn=;dc=x" }, // A hex string may be followed directly by '+' { "cn=#04024869+sn=x,dc=y" }, { "sn=x+cn=#04024869,dc=y" }, // The RFC 2253 separator { "cn=a;dc=x" }, // The AVAs of a multi-valued RDN match whatever their order { "sn=x+cn=y,dc=z" }, { "cn=y+sn=x,dc=z" }, }; } @Test(dataProvider = "patternsWithoutWildcard") public void patternMatchesTheDNParsedFromTheSameString(String pattern) throws Exception { final DN dn = DN.valueOf(pattern); assertThat(PatternDN.decode(pattern).matchesDN(dn)).as("pattern %s against DN %s", pattern, dn).isTrue(); } @DataProvider public Object[][] patternsAndOtherDNs() { return new Object[][] { { "cn=\"a\\2Cb\",dc=x", "cn=a2Cb,dc=x" }, { "cn=+sn=x,dc=y", "cn=\\+sn\\=x,dc=y" }, { "cn=,dc=x", "dc=x" }, { "cn=,dc=x", "cn=\\,dc\\=x" }, { "cn=#04024869+sn=x,dc=y", "cn=#04024869,dc=y" }, { "sn=x+cn=y,dc=z", "sn=y+cn=x,dc=z" }, { "sn=x+cn=y,dc=z", "sn=x+givenName=y,dc=z" }, }; } @Test(dataProvider = "patternsAndOtherDNs") public void patternDoesNotMatchADifferentDN(String pattern, String otherDN) throws Exception { assertThat(PatternDN.decode(pattern).matchesDN(DN.valueOf(otherDN))).isFalse(); } /** Patterns that end in a lone backslash, which DN.valueOf() rejects too. */ @DataProvider public Object[][] patternsWithATrailingBackslash() { return new Object[][] { { "cn=a\\" }, { "cn=\\" }, { "cn=a,dc=x\\" }, { "cn=a*\\" }, }; } /** The pattern is rejected with the result code and the message of DN.valueOf(). */ @Test(dataProvider = "patternsWithATrailingBackslash") public void patternWithATrailingBackslashIsRejected(String pattern) throws Exception { final Throwable patternError = catchThrowable(() -> PatternDN.decode(pattern)); assertThat(patternError).isInstanceOf(DirectoryException.class); assertThat(((DirectoryException) patternError).getResultCode()).isEqualTo(ResultCode.INVALID_DN_SYNTAX); assertThat(((DirectoryException) patternError).getMessageObject().toString()) .isEqualTo(ERR_ATTR_SYNTAX_DN_TRAILING_ESCAPE.get(pattern).toString()); if (!pattern.contains("*")) { final Throwable dnError = catchThrowable(() -> DN.valueOf(pattern)); assertThat(dnError).isInstanceOf(LocalizedIllegalArgumentException.class); assertThat(dnError.getMessage()).isEqualTo(patternError.getMessage()); } } @DataProvider public Object[][] wildcardPatterns() { return new Object[][] { { "cn=#04024869+sn=*,dc=y", "cn=#04024869+sn=x,dc=y" }, { "cn=a\\2Cb*,dc=x", "cn=a\\,bcd,dc=x" }, { "cn=*,dc=x", "cn=a,dc=x" }, }; } @Test(dataProvider = "wildcardPatterns") public void wildcardPatternMatches(String pattern, String dn) throws Exception { assertThat(PatternDN.decode(pattern).matchesDN(DN.valueOf(dn))).isTrue(); } }