Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions opendj-cli/src/main/java/com/forgerock/opendj/cli/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
import org.forgerock.opendj.ldap.AuthorizationException;
import org.forgerock.opendj.ldap.ConnectionException;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.RDN;

/** This class provides utility functions for all the client side tools. */
public final class Utils {
Expand Down Expand Up @@ -541,7 +540,7 @@ public static boolean isDN(String dn) {
* @return The DN of the administrator for the given UID.
*/
public static DN getAdministratorDN(String uid) {
return DN.valueOf(RDN.valueOf("cn=" + uid) + ",cn=Administrators, cn=admin data");
return DN.valueOf("cn=Administrators,cn=admin data").child("cn", uid);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,17 @@
* information: "Portions Copyright [year] [name of copyright owner]".
*
* Copyright 2014-2016 ForgeRock AS.
* Portions Copyright 2026 3A Systems, LLC.
*/
package com.forgerock.opendj.cli;

import java.io.File;
import java.io.IOException;

import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.RDN;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
Expand Down Expand Up @@ -88,5 +93,21 @@ public void isDN() {
assertFalse(Utils.isDN("babs@example.com"));
}

@DataProvider
public Object[][] administratorUIDs() {
return new Object[][] { { "admin" }, { "a,b" }, { "a+b" }, { "#1" }, { " lead" }, { "a\\b" }, { "a;b" },
{ "a=b" }, { "J\u00f6rg" } };
}

@Test(dataProvider = "administratorUIDs")
public void getAdministratorDNKeepsTheWholeUID(final String uid) {
final DN adminDN = Utils.getAdministratorDN(uid);
assertEquals(adminDN.parent(), DN.valueOf("cn=Administrators,cn=admin data"));
final RDN rdn = adminDN.rdn();
assertEquals(rdn.size(), 1);
assertEquals(rdn.getFirstAVA().getAttributeType().getNameOrOID(), "cn");
assertEquals(rdn.getFirstAVA().getAttributeValue(), ByteString.valueOfUtf8(uid));
}


}
27 changes: 16 additions & 11 deletions opendj-core/src/main/java/org/forgerock/opendj/ldap/AVA.java
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,20 @@ public static AVA valueOf(final String ava) {
*/
public static AVA valueOf(final String ava, final Schema schema) {
final SubstringReader reader = new SubstringReader(ava);
final AVA parsedAva;
try {
return decode(reader, schema);
parsedAva = decode(reader, schema);
} catch (final UnknownSchemaElementException e) {
final LocalizableMessage message =
ERR_RDN_TYPE_NOT_FOUND.get(ava, e.getMessageObject());
throw new LocalizedIllegalArgumentException(message);
}
reader.skipWhitespaces();
if (reader.remaining() > 0) {
throw new LocalizedIllegalArgumentException(
ERR_AVA_TRAILING_GARBAGE.get(ava, reader.read(reader.remaining())));
}
return parsedAva;
}

static AVA decode(final SubstringReader reader, final Schema schema) {
Expand Down Expand Up @@ -285,7 +292,7 @@ private static ByteString readAttributeValueAsBER(final SubstringReader reader)

// The rest of the value must be a multiple of two hex
// characters. The end of the value may be designated by the
// end of the DN, a comma or semicolon, or a space.
// end of the DN, a comma or semicolon, a plus sign, or a space.
while (reader.remaining() > 0) {
char c = reader.read();
if (isHexDigit(c)) {
Expand All @@ -303,7 +310,7 @@ private static ByteString readAttributeValueAsBER(final SubstringReader reader)
throw new LocalizedIllegalArgumentException(
ERR_ATTR_SYNTAX_DN_HEX_VALUE_TOO_SHORT.get(reader.getString()));
}
} else if (c == ' ' || c == ',' || c == ';') {
} else if (c == ' ' || c == ',' || c == ';' || c == '+') {
// This denotes the end of the value.
break;
} else {
Expand All @@ -326,14 +333,8 @@ private static ByteString readAttributeValueAsBER(final SubstringReader reader)

private static ByteString readAttributeValue(final SubstringReader reader, final boolean isQuoted) {
reader.reset();
final ByteString bytes = delimitAndEvaluateEscape(reader, isQuoted);
if (bytes.length() == 0) {
// We don't allow an empty attribute value.
final LocalizableMessage message =
ERR_ATTR_SYNTAX_DN_INVALID_REQUIRES_ESCAPE_CHAR.get(reader.getString(), reader.pos());
throw new LocalizedIllegalArgumentException(message);
}
return bytes;
// RFC 4514 allows an empty value wherever it appears: whether the attribute allows it is a schema check.
return delimitAndEvaluateEscape(reader, isQuoted);
}

private static ByteString delimitAndEvaluateEscape(final SubstringReader reader, final boolean isQuoted) {
Expand Down Expand Up @@ -397,6 +398,10 @@ private static ByteString delimitAndEvaluateEscape(final SubstringReader reader,
// We hit the end of the AVA before the closing quote. That's an error.
throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_DN_UNMATCHED_QUOTE.get(reader.getString()));
}
if (escaped) {
// A trailing escape character escapes nothing.
throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_DN_TRAILING_ESCAPE.get(reader.getString()));
}
reader.reset();
valueBuffer.setLength(valueBuffer.length() - trailingSpaces);
return ByteString.valueOfUtf8(valueBuffer);
Expand Down
9 changes: 8 additions & 1 deletion opendj-core/src/main/java/org/forgerock/opendj/ldap/DN.java
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,14 @@ private static DN decode(final SubstringReader reader, final Schema schema, fina

LinkedList<Pair<Integer, RDN>> parentRDNs = null;
DN parent = null;
while (reader.remaining() > 0 && reader.read() == ',') {
while (reader.remaining() > 0) {
// Only a separator may follow an RDN. RFC 2253 also allowed ';', so accept it as ',' rather than
// stopping there and silently returning the DN parsed so far.
final char c = reader.read();
if (c != RDN_CHAR_SEPARATOR && c != ';') {
throw new LocalizedIllegalArgumentException(
ERR_DN_TRAILING_GARBAGE.get(reader.getString(), reader.getString().substring(reader.pos() - 1)));
}
reader.skipWhitespaces();
if (reader.remaining() == 0) {
throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_DN_ATTR_NO_NAME.get(reader.getString()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*
* Copyright 2009 Sun Microsystems, Inc.
* Portions Copyright 2016 ForgeRock AS.
* Portions Copyright 2026 3A Systems, LLC.
*/

package org.forgerock.opendj.ldap.schema;
Expand All @@ -35,6 +36,31 @@
*/
final class NameAndOptionalUIDSyntaxImpl extends AbstractSyntaxImpl {

/**
* Returns the position of the octothorpe (#) that starts the "optional uid" of the provided value, or -1 if the
* value has no "optional uid". The DN may contain an escaped octothorpe, and such an octothorpe does not start
* the "optional uid".
*
* @param value
* The trimmed string representation of a name and optional UID value.
* @return The position of the octothorpe that starts the "optional uid", or -1 if there is none.
*/
static int optionalUidPosition(final String value) {
if (!value.endsWith("'B") && !value.endsWith("'b")) {
return -1;
}
// The bit string cannot contain an octothorpe, so only the last one can start the "optional uid".
final int sharpPos = value.lastIndexOf("#'");
if (sharpPos <= 0) {
return -1;
}
int backslashes = 0;
for (int i = sharpPos - 1; i >= 0 && value.charAt(i) == '\\'; i--) {
backslashes++;
}
return backslashes % 2 == 0 ? sharpPos : -1;
}

@Override
public String getEqualityMatchingRule() {
return EMR_UNIQUE_MEMBER_OID;
Expand Down Expand Up @@ -63,14 +89,8 @@ public boolean valueIsAcceptable(final Schema schema, final ByteSequence value,

// See if the value contains the "optional uid" portion. If we think
// it does, then mark its location.
int dnEndPos = valueLength;
int sharpPos = -1;
if (valueString.endsWith("'B") || valueString.endsWith("'b")) {
sharpPos = valueString.lastIndexOf("#'");
if (sharpPos > 0) {
dnEndPos = sharpPos;
}
}
final int sharpPos = optionalUidPosition(valueString);
final int dnEndPos = sharpPos > 0 ? sharpPos : valueLength;

// Take the DN portion of the string and try to normalize it.
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*
* Copyright 2009 Sun Microsystems, Inc.
* Portions copyright 2014-2016 ForgeRock AS.
* Portions Copyright 2026 3A Systems, LLC.
*/
package org.forgerock.opendj.ldap.schema;

Expand Down Expand Up @@ -41,16 +42,9 @@ final class UniqueMemberEqualityMatchingRuleImpl extends AbstractEqualityMatchin
public ByteString normalizeAttributeValue(final Schema schema, final ByteSequence value) throws DecodeException {
// Separate value into normalized DN and "optional uid" portion.
final String stringValue = value.toString().trim();
int dnEndPosition = stringValue.length();
String optionalUid = "";
int sharpPosition = -1;
if (stringValue.endsWith("'B") || stringValue.endsWith("'b")) {
sharpPosition = stringValue.lastIndexOf("#'");
if (sharpPosition > 0) {
dnEndPosition = sharpPosition;
optionalUid = stringValue.substring(sharpPosition);
}
}
final int sharpPosition = NameAndOptionalUIDSyntaxImpl.optionalUidPosition(stringValue);
final int dnEndPosition = sharpPosition > 0 ? sharpPosition : stringValue.length();
final String optionalUid = sharpPosition > 0 ? stringValue.substring(sharpPosition) : "";
try {
DN dn = DN.valueOf(stringValue.substring(0, dnEndPosition), schema.asNonStrictSchema());
return new ByteStringBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,24 +306,26 @@ final TagResult generateValue(TemplateEntry templateEntry, TemplateValue templat
return TagResult.SUCCESS;
}

String dnAsString = "";
final DN dnToFormat;
if (numComponents == 0) {
// Return the DN of the entry
dnAsString = dn.toString();
dnToFormat = dn;
} else if (numComponents > 0) {
// Return the first numComponents RDNs of the DN
dnAsString = dn.localName(numComponents).toString();
dnToFormat = dn.localName(numComponents);
} else {
// numComponents is negative
// Return the last numComponents RDNs of the DN
dnAsString = dn.parent(dn.size() - Math.abs(numComponents)).toString();
dnToFormat = dn.parent(dn.size() - Math.abs(numComponents));
}
// If expected separator is not standard separator
// Then substitute expected to standard
if (!separator.equals(",")) {
dnAsString = dnAsString.replaceAll(",", separator);
// Join the RDNs with the expected separator: replacing the commas of the DN string
// would also replace the escaped commas inside the attribute values.
for (int i = 0; i < dnToFormat.size(); i++) {
if (i > 0) {
templateValue.append(separator);
}
templateValue.append(dnToFormat.parent(i).rdn());
}
templateValue.append(dnAsString);

return TagResult.SUCCESS;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,13 @@ ERR_RDN_DUPLICATE_AVA_TYPES=An RDN contained multiple components \
ERR_RDN_NO_AVAS=An RDN must contain at least one attribute type and value
ERR_DN_TYPE_NOT_FOUND=The DN "%s" could not be parsed due to the \
following reason: %s
ERR_DN_TRAILING_GARBAGE=The DN "%s" could not be parsed because it \
contained trailing content after an RDN: "%s"
ERR_AVA_TRAILING_GARBAGE=The attribute value assertion "%s" could not be \
parsed because it contained trailing content after the attribute value: "%s"
ERR_ATTR_SYNTAX_DN_TRAILING_ESCAPE=The provided value "%s" could not be \
parsed as a valid distinguished name because it ends with an escape \
character (\\) that is not followed by the character it escapes
ERR_ATTRIBUTE_DESCRIPTION_EMPTY=The attribute description \
"%s" could not be parsed because it was empty
ERR_ATTRIBUTE_DESCRIPTION_ILLEGAL_CHARACTER=The attribute description \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
* information: "Portions Copyright [year] [name of copyright owner]".
*
* Portions copyright 2016 ForgeRock AS.
* Portions Copyright 2026 3A Systems, LLC.
*/
package org.forgerock.opendj.ldap;

import static org.assertj.core.api.Assertions.*;

import org.forgerock.i18n.LocalizedIllegalArgumentException;
import org.forgerock.opendj.ldap.schema.AttributeType;
import org.forgerock.opendj.ldap.schema.Schema;
import org.testng.annotations.DataProvider;
Expand Down Expand Up @@ -80,4 +82,39 @@ public void avaConstructedWithValueContainingLeadingAndTrailingSpacesShouldBeEsc
public void valueOfDecodesTrailingEscapedChars() {
assertThat(AVA.valueOf("dc=\\41\\42\\43").toString()).isEqualTo("dc=ABC");
}

@DataProvider
public Object[][] illegalAVAs() {
// @formatter:off
return new Object[][] {
{ "cn=a,dc=b" },
{ "cn=a;dc=b" },
{ "cn=a+sn=b" },
{ "cn=\"a\"b" },
{ "cn=#0402 junk" },
{ "cn=#0402+sn=b" },
{ "cn=a\\" },
};
// @formatter:on
}

@Test(dataProvider = "illegalAVAs", expectedExceptions = LocalizedIllegalArgumentException.class)
public void valueOfShouldRejectTrailingContent(final String ava) {
AVA.valueOf(ava);
}

@DataProvider
public Object[][] emptyValueAVAs() {
return new Object[][] { { "cn=" }, { "cn= " }, { "cn=\"\"" }, { "cn=\"\" " } };
}

@Test(dataProvider = "emptyValueAVAs")
public void valueOfShouldAcceptAnEmptyValue(final String ava) {
assertThat(AVA.valueOf(ava).getAttributeValue()).isEqualTo(ByteString.empty());
}

@Test
public void valueOfShouldAcceptTrailingSpacesAfterAHexString() {
assertThat(AVA.valueOf("2.5.4.3=#76616C7565 ").getAttributeValue()).isEqualTo(ByteString.valueOfUtf8("value"));
}
}
Loading
Loading