diff --git a/openam-ldap-utils/src/main/java/org/forgerock/openam/ldap/LDAPUtils.java b/openam-ldap-utils/src/main/java/org/forgerock/openam/ldap/LDAPUtils.java index d8140fd82b..40cfe5104c 100644 --- a/openam-ldap-utils/src/main/java/org/forgerock/openam/ldap/LDAPUtils.java +++ b/openam-ldap-utils/src/main/java/org/forgerock/openam/ldap/LDAPUtils.java @@ -12,6 +12,7 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2013-2016 ForgeRock AS. + * Portions Copyright 2026 3A Systems, LLC. */ package org.forgerock.openam.ldap; @@ -632,7 +633,7 @@ public static String normalizeDN(String dn) { * @param orgName The DN string. * @return A DN. */ - final static Pattern dnRule=Pattern.compile("^(?:[A-Za-z][\\w-]*|\\d+(?:\\.\\d+)*)=(?:#(?:[\\dA-Fa-f]{2})+|(?:[^,=\\+<>#;\\\\\"]|\\\\[,=\\+<>#;\\\\\"]|\\\\[\\dA-Fa-f]{2})*|\"(?:[^\\\\\"]|\\\\[,=\\+<>#;\\\\\"]|\\\\[\\dA-Fa-f]{2})*\")(?:\\+(?:[A-Za-z][\\w-]*|\\d+(?:\\.\\d+)*)=(?:#(?:[\\dA-Fa-f]{2})+|(?:[^,=\\+<>#;\\\\\"]|\\\\[,=\\+<>#;\\\\\"]|\\\\[\\dA-Fa-f]{2})*|\"(?:[^\\\\\"]|\\\\[,=\\+<>#;\\\\\"]|\\\\[\\dA-Fa-f]{2})*\"))*(?:,(?:[A-Za-z][\\w-]*|\\d+(?:\\.\\d+)*)=(?:#(?:[\\dA-Fa-f]{2})+|(?:[^,=\\+<>#;\\\\\"]|\\\\[,=\\+<>#;\\\\\"]|\\\\[\\dA-Fa-f]{2})*|\"(?:[^\\\\\"]|\\\\[,=\\+<>#;\\\\\"]|\\\\[\\dA-Fa-f]{2})*\")(?:\\+(?:[A-Za-z][\\w-]*|\\d+(?:\\.\\d+)*)=(?:#(?:[\\dA-Fa-f]{2})+|(?:[^,=\\+<>#;\\\\\"]|\\\\[,=\\+<>#;\\\\\"]|\\\\[\\dA-Fa-f]{2})*|\"(?:[^\\\\\"]|\\\\[,=\\+<>#;\\\\\"]|\\\\[\\dA-Fa-f]{2})*\"))*)*$"); + final static Pattern dnRule=Pattern.compile("^(?:[A-Za-z][\\w-]*|\\d+(?:\\.\\d+)*)=(?:#(?:[\\dA-Fa-f]{2})+| *(?:(?:[^ ,+<>#;\\\\\"]|\\\\[,=\\+<>#;\\\\\"]|\\\\[\\dA-Fa-f]{2})(?:[^,+<>;\\\\\"]|\\\\[,=\\+<>#;\\\\\"]|\\\\[\\dA-Fa-f]{2})*)?|\"(?:[^\\\\\"]|\\\\[,=\\+<>#;\\\\\"]|\\\\[\\dA-Fa-f]{2})*\")(?:\\+(?:[A-Za-z][\\w-]*|\\d+(?:\\.\\d+)*)=(?:#(?:[\\dA-Fa-f]{2})+| *(?:(?:[^ ,+<>#;\\\\\"]|\\\\[,=\\+<>#;\\\\\"]|\\\\[\\dA-Fa-f]{2})(?:[^,+<>;\\\\\"]|\\\\[,=\\+<>#;\\\\\"]|\\\\[\\dA-Fa-f]{2})*)?|\"(?:[^\\\\\"]|\\\\[,=\\+<>#;\\\\\"]|\\\\[\\dA-Fa-f]{2})*\"))*(?:,(?:[A-Za-z][\\w-]*|\\d+(?:\\.\\d+)*)=(?:#(?:[\\dA-Fa-f]{2})+| *(?:(?:[^ ,+<>#;\\\\\"]|\\\\[,=\\+<>#;\\\\\"]|\\\\[\\dA-Fa-f]{2})(?:[^,+<>;\\\\\"]|\\\\[,=\\+<>#;\\\\\"]|\\\\[\\dA-Fa-f]{2})*)?|\"(?:[^\\\\\"]|\\\\[,=\\+<>#;\\\\\"]|\\\\[\\dA-Fa-f]{2})*\")(?:\\+(?:[A-Za-z][\\w-]*|\\d+(?:\\.\\d+)*)=(?:#(?:[\\dA-Fa-f]{2})+| *(?:(?:[^ ,+<>#;\\\\\"]|\\\\[,=\\+<>#;\\\\\"]|\\\\[\\dA-Fa-f]{2})(?:[^,+<>;\\\\\"]|\\\\[,=\\+<>#;\\\\\"]|\\\\[\\dA-Fa-f]{2})*)?|\"(?:[^\\\\\"]|\\\\[,=\\+<>#;\\\\\"]|\\\\[\\dA-Fa-f]{2})*\"))*)*$"); public static DN newDN(String orgName) { if (orgName == null || orgName.startsWith("/") || !dnRule.matcher(orgName).matches()) { return DN.rootDN(); diff --git a/openam-ldap-utils/src/test/java/org/forgerock/openam/ldap/LDAPUtilsTest.java b/openam-ldap-utils/src/test/java/org/forgerock/openam/ldap/LDAPUtilsTest.java index b74b21a08d..4509207059 100644 --- a/openam-ldap-utils/src/test/java/org/forgerock/openam/ldap/LDAPUtilsTest.java +++ b/openam-ldap-utils/src/test/java/org/forgerock/openam/ldap/LDAPUtilsTest.java @@ -12,11 +12,13 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2016 ForgeRock AS. + * Portions Copyright 2026 3A Systems, LLC. */ package org.forgerock.openam.ldap; import static org.assertj.core.api.Assertions.assertThat; +import org.forgerock.opendj.ldap.DN; import org.testng.annotations.Test; /** @@ -61,4 +63,62 @@ public void testIsDNInvalid2() throws Exception { // Then assertThat(validationResult).isFalse(); } + + @Test + public void testIsDNWithEqualsInValue() throws Exception { + // Given + String candidateDN = + "ou=https://accounts.google.com/o/saml2?idpid=12345," + + "ou=default,ou=OrganizationConfig,ou=1.0," + + "ou=sunFMSAML2MetadataService,ou=services,dc=openam,dc=org"; + + // When + boolean validationResult = LDAPUtils.isDN(candidateDN); + + // Then + assertThat(validationResult).isTrue(); + } + + @Test + public void testIsDNWithNonLeadingSharpInValue() throws Exception { + // Given + String candidateDN = "ou=https://idp.example.com/metadata#v1,dc=openam,dc=org"; + + // When + DN dn = LDAPUtils.newDN(candidateDN); + + // Then + assertThat(LDAPUtils.isDN(candidateDN)).isTrue(); + assertThat(dn.size()).isEqualTo(3); + assertThat(LDAPUtils.rdnValueFromDn(dn)).isEqualTo("https://idp.example.com/metadata#v1"); + assertThat(LDAPUtils.isDN("ou=a#,dc=x")).isTrue(); + assertThat(LDAPUtils.isDN("cn=a#b+sn=c#d,ou=e#f+l=g#h")).isTrue(); + } + + @Test + public void testIsDNWithEscapedSharpRoundTrip() throws Exception { + // Given + String candidateDN = "ou=https://idp.example.com/metadata\\#v1,dc=openam,dc=org"; + + // When + String serialised = DN.valueOf(candidateDN).toString(); + + // Then + assertThat(LDAPUtils.isDN(candidateDN)).isTrue(); + assertThat(serialised).isEqualTo("ou=https://idp.example.com/metadata#v1,dc=openam,dc=org"); + assertThat(LDAPUtils.isDN(serialised)).isTrue(); + } + + @Test + public void testNewDNWithLeadingSharpInValue() throws Exception { + // A leading '#' starts a hexstring, so "#x" must be rejected by the pre-check, not by DN.valueOf + assertThat(LDAPUtils.newDN("ou=#04024869,dc=x").size()).isEqualTo(2); + assertThat(LDAPUtils.newDN("ou=#x,dc=x").isRootDN()).isTrue(); + // DN.valueOf skips spaces after '=', so the '#' that follows them is still a leading one + assertThat(LDAPUtils.newDN("ou= #x,dc=x").isRootDN()).isTrue(); + assertThat(LDAPUtils.newDN("ou= a#x,dc=x").size()).isEqualTo(2); + assertThat(LDAPUtils.newDN("ou=a,dc=#x").isRootDN()).isTrue(); + assertThat(LDAPUtils.newDN("ou=a+cn=#x,dc=x").isRootDN()).isTrue(); + assertThat(LDAPUtils.newDN("ou=a,dc=x+cn=#x").isRootDN()).isTrue(); + } }