| | |
| | | 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 { |
| | |
| | | * @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); |
| | | } |
| | | |
| | | /** |
| | |
| | | * 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; |
| | |
| | | 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)); |
| | | } |
| | | |
| | | |
| | | } |
| | |
| | | */ |
| | | 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) { |
| | |
| | | |
| | | // 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)) { |
| | |
| | | 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 { |
| | |
| | | |
| | | 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) { |
| | |
| | | // 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); |
| | |
| | | |
| | | 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())); |
| | |
| | | * |
| | | * Copyright 2009 Sun Microsystems, Inc. |
| | | * Portions Copyright 2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | |
| | | package org.forgerock.opendj.ldap.schema; |
| | |
| | | */ |
| | | 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; |
| | |
| | | |
| | | // 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 { |
| | |
| | | * |
| | | * Copyright 2009 Sun Microsystems, Inc. |
| | | * Portions copyright 2014-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.forgerock.opendj.ldap.schema; |
| | | |
| | |
| | | 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() |
| | |
| | | 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; |
| | | } |
| | |
| | | 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 \ |
| | |
| | | * 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; |
| | |
| | | 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")); |
| | | } |
| | | } |
| | |
| | | { "cn=a\\b" }, |
| | | { "cn=a\\bg" }, |
| | | { "cn=\"hello" }, |
| | | { "cn=+mail=,dc=example,dc=com" }, |
| | | { "cn=xyz+sn=,dc=example,dc=com" }, |
| | | { "cn=,dc=example,dc=com" }, |
| | | { "cn=a+cn=b,dc=example,dc=com" } |
| | | { "cn=a+cn=b,dc=example,dc=com" }, |
| | | // Content after an RDN that is neither a separator nor the end of the DN |
| | | { "cn=\"a\"b,dc=c" }, |
| | | { "cn=\"a\" b" }, |
| | | { "dc=c,cn=\"a\"b" }, |
| | | { "cn=#0402 junk,dc=c" }, |
| | | { "cn=#0402 junk" }, |
| | | { "cn=Jim;" }, |
| | | { "cn=Jim; " }, |
| | | { "cn=Jim;dc=c;" }, |
| | | // A trailing escape character that escapes nothing |
| | | { "cn=a\\" }, |
| | | { "cn=a\\\\\\" }, |
| | | { "cn=a\\,dc=b\\" }, |
| | | }; |
| | | // @formatter:on |
| | | } |
| | |
| | | assertEquals(raw, string); |
| | | } |
| | | |
| | | @DataProvider |
| | | public Object[][] rfc4514DNs() { |
| | | // @formatter:off |
| | | return new Object[][] { |
| | | // The RFC 2253 RDN separator ';' separates RDNs, it does not end the DN |
| | | { "cn=a;dc=b", "cn=a,dc=b", 2 }, |
| | | { "uid=a,ou=People;dc=example,dc=com", "uid=a,ou=People,dc=example,dc=com", 4 }, |
| | | { "cn=a ; dc=b", "cn=a,dc=b", 2 }, |
| | | { "cn=\"a\";dc=b", "cn=a,dc=b", 2 }, |
| | | { "cn=#0402 ;dc=b", "cn=\\04\\02,dc=b", 2 }, |
| | | // A hex string may be followed directly by '+' |
| | | { "cn=#04024869+sn=x", "cn=\\04\\02Hi+sn=x", 1 }, |
| | | { "cn=#04024869+sn=x,dc=y", "cn=\\04\\02Hi+sn=x,dc=y", 2 }, |
| | | { "sn=x+cn=#04024869,dc=y", "sn=x+cn=\\04\\02Hi,dc=y", 2 }, |
| | | // An empty value is accepted wherever it appears |
| | | { "cn=,dc=x", "cn=,dc=x", 2 }, |
| | | { "cn=\"\",dc=x", "cn=,dc=x", 2 }, |
| | | { "cn= ,dc=x", "cn=,dc=x", 2 }, |
| | | { "cn=x+sn=,dc=y", "cn=x+sn=,dc=y", 2 }, |
| | | { "sn=+cn=x,dc=y", "sn=+cn=x,dc=y", 2 }, |
| | | { "cn=+mail=,dc=example,dc=com", "cn=+mail=,dc=example,dc=com", 3 }, |
| | | { "dc=x,cn=", "dc=x,cn=", 2 }, |
| | | // Unescaped '=' and a non-leading '#' are legal in a value |
| | | { "cn=a=b", "cn=a\\=b", 1 }, |
| | | { "cn==", "cn=\\=", 1 }, |
| | | { "cn=a#b", "cn=a#b", 1 }, |
| | | { "cn=a#", "cn=a#", 1 }, |
| | | { "ou=https://idp.example.com/metadata#v1,dc=x", "ou=https://idp.example.com/metadata#v1,dc=x", 2 }, |
| | | }; |
| | | // @formatter:on |
| | | } |
| | | |
| | | @Test(dataProvider = "rfc4514DNs") |
| | | public void valueOfShouldParseTheWholeString(final String dn, final String expectedString, final int size) { |
| | | final DN parsed = DN.valueOf(dn); |
| | | assertThat(parsed.toString()).isEqualTo(expectedString); |
| | | assertThat(parsed.size()).isEqualTo(size); |
| | | assertEquals(DN.valueOf(parsed.toString()), parsed); |
| | | assertEquals(DN.valueOf(ByteString.valueOfUtf8(dn)), parsed); |
| | | } |
| | | |
| | | @Test |
| | | public void toStringOfChildWithEmptyValueShouldBeParseable() { |
| | | final DN child = DN.valueOf("dc=x").child("cn", ""); |
| | | assertThat(child.toString()).isEqualTo("cn=,dc=x"); |
| | | assertEquals(DN.valueOf(child.toString()), child); |
| | | } |
| | | |
| | | @Test |
| | | public void valueOfShouldNotTruncateWhenParentIsCached() { |
| | | // The parent cache is keyed by the string after the separator: a cached parent must not hide garbage. |
| | | DN.valueOf("cn=a,dc=example,dc=com"); |
| | | assertEquals(DN.valueOf("cn=b;dc=example,dc=com"), DN.valueOf("cn=b,dc=example,dc=com")); |
| | | try { |
| | | DN.valueOf("cn=\"b\"x,dc=example,dc=com"); |
| | | fail("Expected LocalizedIllegalArgumentException"); |
| | | } catch (LocalizedIllegalArgumentException expected) { |
| | | // Expected. |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Test data for testInScopeOf tests. |
| | | * |
| | |
| | | { "oid.1." }, { "1.3.6.1.4.1.1466..0=#04024869" }, { "cn=#a" }, { "cn=#ag" }, |
| | | { "cn=#ga" }, { "cn=#abcdefgh" }, |
| | | { "cn=a\\b" }, // { "cn=a\\bg" }, { "cn=\"hello" }, |
| | | { "cn=+mail=,dc=example,dc=com" }, { "cn=xyz+sn=,dc=example,dc=com" }, |
| | | { "cn=,dc=example,dc=com" } }; |
| | | // Content after an RDN must not be dropped silently |
| | | { "cn=\"a\"b,dc=c" }, { "cn=#0402 junk,dc=c" }, { "cn=a\\" } }; |
| | | } |
| | | |
| | | @Override |
| | |
| | | "cn=j. smith+ou=sales,dc=example,dc=net", ConditionResult.TRUE }, |
| | | { "cn=John+a=Doe", "a=Doe+cn=john", ConditionResult.TRUE }, |
| | | { "O=\"Sue, Grabbit and Runn\",C=US", "o=sue\\, grabbit and runn,c=us", |
| | | ConditionResult.TRUE }, }; |
| | | ConditionResult.TRUE }, |
| | | // An empty value is accepted wherever it appears |
| | | { "cn=+mail=,dc=example,dc=com", "mail=+cn=,dc=example,dc=com", ConditionResult.TRUE }, |
| | | { "cn=xyz+sn=,dc=example,dc=com", "sn=+cn=xyz,dc=example,dc=com", ConditionResult.TRUE }, |
| | | { "cn=,dc=example,dc=com", "cn=\"\",dc=example,dc=com", ConditionResult.TRUE }, |
| | | // ';' separates RDNs: the DN after it is neither dropped nor ignored by the match |
| | | { "uid=bob,ou=People;dc=example,dc=com", "uid=bob,ou=People,dc=example,dc=com", ConditionResult.TRUE }, |
| | | { "uid=bob,ou=People,dc=example,dc=com;uid=alice,ou=People,dc=example,dc=com", |
| | | "uid=bob,ou=People,dc=example,dc=com", ConditionResult.FALSE }, |
| | | { "cn=#04024869+sn=x,dc=y", "sn=x+cn=hi,dc=y", ConditionResult.TRUE }, }; |
| | | } |
| | | |
| | | /** |
| 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.forgerock.opendj.ldap.schema; |
| | | |
| | | import static org.forgerock.opendj.ldap.schema.SchemaConstants.SYNTAX_NAME_AND_OPTIONAL_UID_OID; |
| | | |
| | | import org.testng.annotations.DataProvider; |
| | | import org.testng.annotations.Test; |
| | | |
| | | /** Name and optional UID syntax tests. */ |
| | | @Test |
| | | public class NameAndOptionalUIDSyntaxTest extends AbstractSyntaxTestCase { |
| | | @Override |
| | | @DataProvider(name = "acceptableValues") |
| | | public Object[][] createAcceptableValues() { |
| | | return new Object[][] { |
| | | { "dc=example,dc=com", true }, |
| | | { "dc=example,dc=com#'0101'B", true }, |
| | | { "dc=example,dc=com#'0102'B", false }, |
| | | // The escaped '#' is part of the last value "a#'01'B", so there is no uid to check |
| | | { "dc=x,o=a\\#'01'B", true }, |
| | | { "dc=x,o=a\\#'02'B", true }, |
| | | // An escaped backslash does not escape the '#' that follows it |
| | | { "dc=x,o=a\\\\#'01'B", true }, |
| | | { "dc=x,o=a\\\\#'02'B", false }, |
| | | // Not a DN |
| | | { "dc=x,o=\"a\"b#'01'B", false }, |
| | | }; |
| | | } |
| | | |
| | | @Override |
| | | protected Syntax getRule() { |
| | | return Schema.getCoreSchema().getSyntax(SYNTAX_NAME_AND_OPTIONAL_UID_OID); |
| | | } |
| | | } |
| | |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2014-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.forgerock.opendj.ldap.schema; |
| | | |
| | |
| | | "1.3.6.1.4.1.1466.0=#04024869,O=Test,C=GB#'0101'B", ConditionResult.TRUE }, |
| | | { "1.3.6.1.4.1.1466.0=#04024869,O=Test,C=GB#'0101'B", |
| | | "1.3.6.1.4.1.1466.0=#04024869,o=Test,C=GB#'0101'B", ConditionResult.TRUE }, |
| | | // An escaped '#' belongs to the DN: "a#'01'B" is the last value, there is no optional uid |
| | | { "dc=x,o=a\\#'01'B", "dc=x,o=a\\#'01'B", ConditionResult.TRUE }, |
| | | { "dc=x,o=a\\#'01'B", "dc=x,o=a\\23'01'B", ConditionResult.TRUE }, |
| | | { "dc=x,o=a\\#'01'B", "dc=x,o=a#'01'B", ConditionResult.FALSE }, |
| | | // An escaped backslash does not escape the '#' that follows it |
| | | { "dc=x,o=a\\\\#'01'B", "dc=x,o=a\\5C#'01'B", ConditionResult.TRUE }, |
| | | { "dc=x,o=a\\\\#'01'B", "dc=x,o=a\\5C\\23'01'B", ConditionResult.FALSE }, |
| | | }; |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | @Test |
| | | public void testUnderscoreDNTagKeepsEscapedCommas() throws Exception { |
| | | TemplateTag tag = new TemplateTag.UnderscoreDNTag(); |
| | | tagWithArguments(tag); |
| | | |
| | | TemplateValue value = new TemplateValue(NULL_LINE); |
| | | tag.generateValue(templateEntry(DN.valueOf("cn=Smith\\, John,dc=example,dc=com")), value); |
| | | |
| | | assertThat(value.getValueAsString()).isEqualTo("cn=Smith\\, John_dc=example_dc=com"); |
| | | } |
| | | |
| | | @Test |
| | | public void testUnderscoreDNTagTwoComponentsKeepsEscapedCommas() throws Exception { |
| | | TemplateTag tag = new TemplateTag.UnderscoreDNTag(); |
| | | tagWithArguments(tag, "2"); |
| | | |
| | | TemplateValue value = new TemplateValue(NULL_LINE); |
| | | tag.generateValue(templateEntry(DN.valueOf("cn=Smith\\, John,ou=a\\,b,dc=com")), value); |
| | | |
| | | assertThat(value.getValueAsString()).isEqualTo("cn=Smith\\, John_ou=a\\,b"); |
| | | } |
| | | |
| | | @Test |
| | | public void testDNTagOneComponent() throws Exception { |
| | | TemplateTag tag = new TemplateTag.DNTag(); |
| | | tagWithArguments(tag, "1"); |
| | |
| | | |
| | | ==== |
| | | |
| | | [#upgrade-dn-parsing] |
| | | .To Check Stored DNs When Upgrading From a Release Before 5.2.0 |
| | | ==== |
| | | Before 5.2.0, the server read a DN only up to the first `;`, up to the end of a quoted value, or up to a space after a hex string value, and silently dropped the rest. Since 5.2.0, it reads the whole string: `;` separates RDNs as `,` does (RFC 2253), any other content after an RDN is rejected, and an attribute value may be empty, as in `dc=,dc=example,dc=com`. Data written by clients before the upgrade can be affected as follows: |
| | | |
| | | . A value with DN syntax that a client wrote with `;`, such as `member: cn=a;dc=example,dc=com`, was indexed under the key of `cn=a`. It now reads as `cn=a,dc=example,dc=com`, so an indexed equality search for that DN misses the entry until the index is rebuilt. The same holds for a value that was stored while its syntax was not enforced, and that only now parses. |
| | | + |
| | | The `upgrade` command therefore offers to verify the equality indexes of the attributes that hold DNs, such as `member`, `uniqueMember`, `owner` or `seeAlso`, in every enabled backend at the end of the upgrade, and rebuilds them under each base DN where they do not match the entries. The verification reads every entry of these backends, so on a large backend it takes a while; the indexes are only rebuilt where it finds a missing key. The default answer is yes, so `upgrade --no-prompt` performs the verification too, as do the Docker image and the native packages, which run `upgrade --no-prompt --force`. If you declined it, run `verify-index` on these indexes with the server stopped, and rebuild them where it reports errors, as described in xref:../admin-guide/chap-indexing.adoc#rebuild-index["Rebuilding Indexes"] in the __Administration Guide__. Do the same for a backend that the upgrade could not read, such as a JDBC or Cassandra backend whose database was unreachable: the upgrade then warns, names the indexes and the base DN, and goes on, leaving these indexes as they were. A rebuild that the upgrade started and that fails, for instance because the temporary directory is full, fails the upgrade, as the index rebuilds of other upgrade tasks do: such indexes may be left untrusted, so that searches cannot use them, until they are rebuilt. The upgrade then neither verifies nor rebuilds the indexes of the base DNs that come after, as the same cause would most likely make their rebuild fail too: it names them, and you verify them and rebuild them where needed once the cause is fixed. |
| | | + |
| | | A stored DN value with other content after an RDN, such as `cn="a"x,dc=example,dc=com`, no longer parses: replace it with the DN that was meant. |
| | | |
| | | . An ACI whose target, `userdn` or `groupdn` DN uses `;` between RDNs now names the whole DN, not the part before the first `;`. An ACI whose DN has other content after an RDN no longer decodes: the server logs a warning naming the ACI when it loads it, and enters lockdown mode. Correct the ACI, then leave lockdown mode as described in xref:../admin-guide/chap-troubleshooting.adoc#troubleshoot-use-lockdown-mode["Preventing Access While You Fix Issues"] in the __Administration Guide__. A global ACI (`ds-cfg-global-aci` in `config.ldif`) of this form stops the server from starting instead, with an error that names the `ds-cfg-global-aci` attribute of the access control handler: correct it in `config.ldif` while the server is stopped. |
| | | |
| | | The JMX names of monitor MBeans whose RDN value holds characters other than letters, digits and spaces change: these characters are now percent-encoded instead of dropped, so that two monitors no longer share a name. For example, `cn-LDAP_Connection_Handler_0000_port_1389` becomes `cn-LDAP_Connection_Handler_0%2E0%2E0%2E0_port_1389`, and the names of the replication monitors, which hold `(`, `)` and `:`, change too. Update the JMX monitoring rules, such as exporter rules or checks, that match these names after the upgrade. The SNMP connection handler finds the connection handlers under their new names. |
| | | |
| | | ==== |
| | | |
| | | [#upgrade-repl] |
| | | .To Upgrade Replicated Servers |
| | | ==== |
| | |
| | | |
| | | Do not upgrade all replicated servers at once in parallel, as this removes all replication changelog data simultaneously, breaking replication. |
| | | ====== |
| | | |
| | | [IMPORTANT] |
| | | ====== |
| | | When the topology still has servers before 5.2.0, do not add or rename entries whose DN holds an empty attribute value, such as `dc=,dc=example,dc=com` or `cn=x+sn=,dc=example,dc=com`, until every server runs 5.2.0 or later. A server before 5.2.0 cannot read the DN of such a change: it closes its replication connection when it receives the change, receives the same change again when it reconnects, and so stops replicating altogether. Such an entry cannot be imported into a server before 5.2.0 either. |
| | | ====== |
| | | For each server in the replication topology, follow these steps: |
| | | |
| | | . Direct client application traffic away from the server to upgrade. |
| | |
| | | logs`, and where the server is up at all the container is left running to be looked at, |
| | | turning `unhealthy` once the start period is over. |
| | | |
| | | Started over an instance of an older version, the container runs `upgrade --no-prompt --force` |
| | | before the server: the upgrade tasks that take long, such as verifying or rebuilding indexes, |
| | | are performed then, since nobody is there to run them afterwards. On a large instance they can |
| | | take longer than the start period, so the container may report `unhealthy` until the upgrade |
| | | is over; `docker logs` shows which task is running. |
| | | |
| | | The server runs as PID 1 of the container, and a JVM does not reap the processes left |
| | | behind to it - those of a health check that ran past its timeout, say. Run the container |
| | | with `docker run --init` (`init: true` in Compose) to put a PID 1 in front of the server |
| | |
| | | # Instance dir does exist? We start opendj without detach |
| | | if [ -d ./data/config ]; then |
| | | # nothing is bootstrapped here, the instance is already there - but a half-migrated one |
| | | # is not ready to serve either, so the marker follows the upgrade |
| | | if sh ./upgrade -n; then |
| | | # is not ready to serve either, so the marker follows the upgrade. |
| | | # --force performs the tasks that -n alone answers with their default no, such as rebuilding indexes: |
| | | # nobody is there to run them by hand afterwards, as the native packages do too |
| | | if sh ./upgrade -n --force; then |
| | | # A server whose volume holds the data of the topology is ready as soon as it serves: |
| | | # gating it on its peers would deadlock a whole-cluster restart under OrderedReady, |
| | | # where -0 would wait for peers the StatefulSet starts only once -0 is ready - and so |
| | |
| | | } |
| | | |
| | | // Create and register monitors. |
| | | statTracker = new LDAPStatistics(handlerName + " Statistics"); |
| | | statTracker = new LDAPStatistics(DN.escapeAttributeValue(handlerName) + " Statistics"); |
| | | DirectoryServer.registerMonitorProvider(statTracker); |
| | | |
| | | connMonitor = new ClientConnectionMonitorProvider(this); |
| | |
| | | * |
| | | * Copyright 2008-2009 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.guitools.controlpanel.task; |
| | | |
| | |
| | | } |
| | | newBaseDNs.removeAll(dnsToRemove); |
| | | |
| | | String backendName = backend.getBackendID(); |
| | | DN dn = DN.valueOf("ds-cfg-backend-id=" + backendName + ",cn=Backends,cn=config"); |
| | | DN dn = Utilities.getBackendConfigDN(backend.getBackendID()); |
| | | updateConfigEntryWithAttribute(dn, ATTR_BACKEND_BASE_DN, newBaseDNs); |
| | | } |
| | | |
| | |
| | | */ |
| | | private void deleteBackend(BackendDescriptor backend) throws OpenDsException, ConfigException |
| | | { |
| | | DN dn = DN.valueOf("ds-cfg-backend-id" + "=" + backend.getBackendID() + ",cn=Backends,cn=config"); |
| | | DN dn = Utilities.getBackendConfigDN(backend.getBackendID()); |
| | | Utilities.deleteConfigSubtree(getServerContext().getConfigurationHandler(), dn); |
| | | } |
| | | |
| | |
| | | */ |
| | | private void deleteIndex(AbstractIndexDescriptor index) throws OpenDsException |
| | | { |
| | | final String backendId = "ds-cfg-backend-id" + "=" + index.getBackend().getBackendID(); |
| | | String dn; |
| | | if (isVLVIndex(index)) |
| | | { |
| | | dn = "ds-cfg-name" + "=" + index.getName() + ",cn=VLV Index," + backendId + ",cn=Backends,cn=config"; |
| | | } |
| | | else |
| | | { |
| | | dn = "ds-cfg-attribute" + "=" + index.getName() + ",cn=Index," + backendId + ",cn=Backends,cn=config"; |
| | | } |
| | | DirectoryServer.getInstance().getServerContext().getConfigurationHandler().deleteEntry(DN.valueOf(dn)); |
| | | DN dn = getIndexConfigDN(index.getBackend().getBackendID(), index.getName(), isVLVIndex(index)); |
| | | DirectoryServer.getInstance().getServerContext().getConfigurationHandler().deleteEntry(dn); |
| | | } |
| | | |
| | | /** |
| | |
| | | * |
| | | * Copyright 2008-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.guitools.controlpanel.ui; |
| | | |
| | |
| | | import javax.swing.SwingUtilities; |
| | | |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.i18n.LocalizedIllegalArgumentException; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.RDN; |
| | | import org.opends.guitools.controlpanel.browser.BrowserController; |
| | | import org.opends.guitools.controlpanel.event.ConfigurationChangeEvent; |
| | | import org.opends.guitools.controlpanel.task.NewEntryTask; |
| | |
| | | |
| | | /** The parent node that was selected when the user clicked on the new entry action. */ |
| | | protected BasicNode parentNode; |
| | | |
| | | /** |
| | | * Returns the DN of a new entry, with the naming value escaped: a value that contains ',', '+' or ';', |
| | | * or starts with '#', is part of the RDN, not a DN separator. |
| | | * |
| | | * @param rdnAttribute |
| | | * the naming attribute. |
| | | * @param rdnValue |
| | | * the naming value typed by the user. |
| | | * @param parentDN |
| | | * the DN of the parent entry. |
| | | * @return the string representation of the DN of the new entry. |
| | | */ |
| | | static String getNewEntryDN(String rdnAttribute, String rdnValue, DN parentDN) |
| | | { |
| | | return parentDN.child(rdnAttribute, rdnValue).toString(); |
| | | } |
| | | |
| | | /** |
| | | * Returns the DN of a new entry, with the naming value escaped, under a parent DN typed by the user. |
| | | * |
| | | * @param rdnAttribute |
| | | * the naming attribute. |
| | | * @param rdnValue |
| | | * the naming value typed by the user. |
| | | * @param parentDN |
| | | * the DN of the parent entry, as typed by the user. |
| | | * @return the string representation of the DN of the new entry. If the parent DN is not a valid DN yet, |
| | | * the escaped RDN followed by the parent DN as typed. |
| | | */ |
| | | static String getNewEntryDN(String rdnAttribute, String rdnValue, String parentDN) |
| | | { |
| | | try |
| | | { |
| | | return getNewEntryDN(rdnAttribute, rdnValue, DN.valueOf(parentDN)); |
| | | } |
| | | catch (LocalizedIllegalArgumentException e) |
| | | { |
| | | return new RDN(rdnAttribute, rdnValue) + "," + parentDN; |
| | | } |
| | | } |
| | | /** The browser controller. */ |
| | | protected BrowserController controller; |
| | | |
| | |
| | | // If it takes time to read the entry, the rdnAttribute might not be initialized yet. Don't try to use it then. |
| | | if (value.length() > 0 && rdnAttribute != null) |
| | | { |
| | | dn.setText(rdnAttribute + "=" + value + "," + parentDN.getText().trim()); |
| | | dn.setText(getNewEntryDN(rdnAttribute, value, parentDN.getText().trim())); |
| | | } |
| | | else |
| | | { |
| | |
| | | * |
| | | * Copyright 2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.guitools.controlpanel.ui; |
| | | |
| | |
| | | String value = name.getText().trim(); |
| | | if (value.length() > 0) |
| | | { |
| | | dn.setText("dc" + "=" + value + "," + parentNode.getDN()); |
| | | dn.setText(getNewEntryDN("dc", value, parentNode.getDN())); |
| | | } |
| | | else |
| | | { |
| | |
| | | String value = name.getText().trim(); |
| | | if (value.length() > 0) |
| | | { |
| | | dn.setText("cn" + "=" + value + "," + parentNode.getDN()); |
| | | dn.setText(getNewEntryDN("cn", value, parentNode.getDN())); |
| | | } |
| | | else |
| | | { |
| | |
| | | * |
| | | * Copyright 2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014-2016 ForgeRock AS |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.guitools.controlpanel.ui; |
| | | |
| | |
| | | String value = name.getText().trim(); |
| | | if (value.length() > 0) |
| | | { |
| | | dn.setText("o" + "=" + value + "," + parentNode.getDN()); |
| | | dn.setText(getNewEntryDN("o", value, parentNode.getDN())); |
| | | } |
| | | else |
| | | { |
| | |
| | | * |
| | | * Copyright 2008-2009 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.guitools.controlpanel.ui; |
| | | |
| | |
| | | String value = name.getText().trim(); |
| | | if (value.length() > 0) |
| | | { |
| | | dn.setText("ou" + "=" + value + "," + parentNode.getDN()); |
| | | dn.setText(getNewEntryDN("ou", value, parentNode.getDN())); |
| | | } |
| | | else |
| | | { |
| | |
| | | * |
| | | * Copyright 2008-2009 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.guitools.controlpanel.ui; |
| | | |
| | |
| | | if (attr.equalsIgnoreCase(NAMING_ATTRIBUTES[i])) |
| | | { |
| | | String value = NAMING_ATTRIBUTE_TEXTFIELDS[i].getText().trim(); |
| | | dn.setText(attr + "=" + value + "," + parentNode.getDN()); |
| | | dn.setText(getNewEntryDN(attr, value, parentNode.getDN())); |
| | | break; |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | /** |
| | | * Returns the DN of the configuration entry of a backend. The backend ID becomes the value of the RDN, so it may |
| | | * hold any character, a ',' for instance. |
| | | * |
| | | * @param backendID |
| | | * the ID of the backend |
| | | * @return the DN of the configuration entry of the backend |
| | | */ |
| | | public static DN getBackendConfigDN(String backendID) |
| | | { |
| | | return DN.valueOf(ConfigConstants.DN_BACKEND_BASE).child(ConfigConstants.ATTR_BACKEND_ID, backendID); |
| | | } |
| | | |
| | | /** |
| | | * Returns the DN of the configuration entry of an index. The backend ID and the index name become the values of |
| | | * RDNs, so they may hold any character. |
| | | * |
| | | * @param backendID |
| | | * the ID of the backend of the index |
| | | * @param indexName |
| | | * the name of the index: the attribute of an attribute index, the name of a VLV index |
| | | * @param isVLVIndex |
| | | * whether the index is a VLV index |
| | | * @return the DN of the configuration entry of the index |
| | | */ |
| | | public static DN getIndexConfigDN(String backendID, String indexName, boolean isVLVIndex) |
| | | { |
| | | final DN backendDN = getBackendConfigDN(backendID); |
| | | return isVLVIndex ? backendDN.child("cn", "VLV Index").child("ds-cfg-name", indexName) |
| | | : backendDN.child("cn", "Index").child("ds-cfg-attribute", indexName); |
| | | } |
| | | |
| | | /** |
| | | * Unescapes UTF-8 text and generates a String from it. |
| | | * @param v the string in UTF-8 format. |
| | | * @return the string with unescaped characters. |
| | |
| | | for (int i = 0; i < stringBytes.length; i++) |
| | | { |
| | | if (stringBytes[i] == '\\' |
| | | && i + 1 < stringBytes.length |
| | | && stringBytes[i+1] == '\\') |
| | | { |
| | | // An escaped backslash: the character after it does not start a hex pair. |
| | | decodedBytes[pos++] = stringBytes[i++]; |
| | | decodedBytes[pos++] = stringBytes[i]; |
| | | } |
| | | else if (stringBytes[i] == '\\' |
| | | && i + 2 < stringBytes.length |
| | | && StaticUtils.isHexDigit(stringBytes[i+1]) |
| | | && StaticUtils.isHexDigit(stringBytes[i+2])) |
| | |
| | | try |
| | | { |
| | | // Read the configuration file. |
| | | DN dn = DN.valueOf("ds-cfg-backend-id" + "=" + backendName + ",cn=Backends,cn=config"); |
| | | DN dn = Utilities.getBackendConfigDN(backendName); |
| | | Utilities.deleteConfigSubtree(DirectoryServer.getInstance().getServerContext().getConfigurationHandler(), dn); |
| | | } |
| | | catch (OpenDsException | ConfigException ode) |
| | |
| | | * |
| | | * Copyright 2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014-2016 ForgeRock AS. |
| | | * Portions 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.opends.messages.AccessControlMessages.*; |
| | | import static org.opends.messages.SchemaMessages.*; |
| | | import static org.opends.server.util.CollectionUtils.*; |
| | |
| | | // Look at the first character. If it is an octothorpe (#), then |
| | | // that means that the value should be a hex string. |
| | | char c = dnString.charAt(pos++); |
| | | if (c == '#') |
| | | if (c == ',' || c == ';' || c == '+') |
| | | { |
| | | // The value is empty and followed by the next RDN or AVA, as DN.valueOf() reads it. |
| | | attributeValues.add(ByteString.empty()); |
| | | return pos - 1; |
| | | } |
| | | else if (c == '#') |
| | | { |
| | | // The first two characters must be hex characters. |
| | | StringBuilder hexString = new StringBuilder(); |
| | |
| | | |
| | | // 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 (pos < length) |
| | | { |
| | | c = dnString.charAt(pos++); |
| | |
| | | throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message); |
| | | } |
| | | } |
| | | else if (c == ' ' || c == ',' || c == ';') |
| | | else if (c == ' ' || c == ',' || c == ';' || c == '+') |
| | | { |
| | | // This denotes the end of the value. |
| | | pos--; |
| | |
| | | // Keep reading until we find an unescaped closing quotation mark. |
| | | boolean escaped = false; |
| | | StringBuilder valueString = new StringBuilder(); |
| | | StringBuilder hexChars = new StringBuilder(); |
| | | while (true) |
| | | { |
| | | if (pos >= length) |
| | |
| | | c = dnString.charAt(pos++); |
| | | if (escaped) |
| | | { |
| | | // The previous character was an escape, so we'll take this |
| | | // one no matter what. |
| | | valueString.append(c); |
| | | // The previous character was an escape. As in an unquoted value, and as DN.valueOf() reads |
| | | // a quoted value, an escaped pair of hex digits is one byte of the UTF-8 encoded value. |
| | | if (isHexDigit(c) && pos < length && isHexDigit(dnString.charAt(pos))) |
| | | { |
| | | hexChars.append(c); |
| | | hexChars.append(dnString.charAt(pos++)); |
| | | } |
| | | else |
| | | { |
| | | appendHexChars(dnString, valueString, hexChars); |
| | | valueString.append(c); |
| | | } |
| | | escaped = false; |
| | | } |
| | | else if (c == '\\') |
| | |
| | | else if (c == '"') |
| | | { |
| | | // This is the end of the value. |
| | | appendHexChars(dnString, valueString, hexChars); |
| | | break; |
| | | } |
| | | else |
| | | { |
| | | // This is just a regular character that should be in the |
| | | // value. |
| | | appendHexChars(dnString, valueString, hexChars); |
| | | valueString.append(c); |
| | | } |
| | | } |
| | |
| | | { |
| | | if (pos >= length) |
| | | { |
| | | if (escaped) |
| | | { |
| | | // A lone backslash at the end, which DN.valueOf() rejects with the same message. |
| | | throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, |
| | | ERR_ATTR_SYNTAX_DN_TRAILING_ESCAPE.get(dnString)); |
| | | } |
| | | // This is the end of the DN and therefore the end of the value. |
| | | // If there are any hex characters, then we need to deal with them accordingly. |
| | | appendHexChars(dnString, valueString, hexChars); |
| | |
| | | import static org.opends.server.util.CollectionUtils.*; |
| | | |
| | | import java.util.Arrays; |
| | | import java.util.Iterator; |
| | | import java.util.List; |
| | | import java.util.TreeMap; |
| | | |
| | |
| | | return false; |
| | | } |
| | | |
| | | // Sort the attribute-value pairs by attribute type. |
| | | // Index the value patterns by attribute type. |
| | | TreeMap<String, List<ByteString>> patternMap = new TreeMap<>(); |
| | | for (int i = 0; i < typePatterns.length; i++) |
| | | { |
| | |
| | | } |
| | | patternMap.put(type.getNameOrOID(), valuePatterns.get(i)); |
| | | } |
| | | if (patternMap.size() != rdn.size()) |
| | | { |
| | | return false; |
| | | } |
| | | |
| | | Iterator<String> patternKeyIter = patternMap.keySet().iterator(); |
| | | // An RDN keeps its AVAs in the order of the DN string, so look each one up by its type. |
| | | for (AVA ava : rdn) |
| | | { |
| | | String rdnKey = ava.getAttributeType().getNameOrOID(); |
| | | if (!rdnKey.equals(patternKeyIter.next()) |
| | | || !matchValuePattern(patternMap.get(rdnKey), ava)) |
| | | List<ByteString> valuePattern = patternMap.get(ava.getAttributeType().getNameOrOID()); |
| | | if (valuePattern == null || !matchValuePattern(valuePattern, ava)) |
| | | { |
| | | return false; |
| | | } |
| | |
| | | public static KeywordBindRule decode(String expression, |
| | | EnumBindRuleType type) |
| | | throws AciException { |
| | | String[] vals=expression.split("#"); |
| | | if(vals.length != 2) { |
| | | // The attribute name cannot contain an octothorpe, but the value after it can. |
| | | final int sharpPos = expression.indexOf('#'); |
| | | if (sharpPos < 0 || sharpPos == expression.length() - 1) { |
| | | LocalizableMessage message = |
| | | WARN_ACI_SYNTAX_INVALID_USERATTR_EXPRESSION.get(expression); |
| | | throw new AciException(message); |
| | | } |
| | | final String[] vals = { expression.substring(0, sharpPos), expression.substring(sharpPos + 1) }; |
| | | UserAttrType userAttrType = UserAttrType.getType(vals[1]); |
| | | switch (userAttrType) { |
| | | case GROUPDN: |
| | |
| | | import java.util.concurrent.ConcurrentMap; |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.opendj.config.server.ConfigChangeResult; |
| | |
| | | try |
| | | { |
| | | env = new Environment(backendDirectory, envConfig); |
| | | monitor = new JEMonitor(config.getBackendId() + " JE Database", env); |
| | | monitor = new JEMonitor(DN.escapeAttributeValue(config.getBackendId()) + " JE Database", env); |
| | | DirectoryServer.registerMonitorProvider(monitor); |
| | | } |
| | | catch (DatabaseException e) |
| | |
| | | import java.util.Set; |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.opendj.config.server.ConfigChangeResult; |
| | |
| | | |
| | | db.initialize(); |
| | | volume = db.loadVolume(VOLUME_NAME); |
| | | monitor = new PDBMonitor(config.getBackendId() + " PDB Database", db); |
| | | monitor = new PDBMonitor(DN.escapeAttributeValue(config.getBackendId()) + " PDB Database", db); |
| | | DirectoryServer.registerMonitorProvider(monitor); |
| | | } |
| | | catch(final InUseException e) { |
| | |
| | | { |
| | | if (monitor == null) |
| | | { |
| | | monitor = new BackendMonitor(backendId + " Storage", this); |
| | | monitor = new BackendMonitor(DN.escapeAttributeValue(backendId) + " Storage", this); |
| | | } |
| | | return monitor; |
| | | } |
| | |
| | | * |
| | | * Portions Copyright 2006-2007-2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2013-2016 ForgeRock AS. |
| | | * Portions Copyright 2023-2025 3A Systems LLC. |
| | | * Portions Copyright 2023-2026 3A Systems LLC. |
| | | */ |
| | | package org.opends.server.config; |
| | | |
| | |
| | | import static org.opends.server.protocols.internal.Requests.newSearchRequest; |
| | | import static org.opends.server.util.CollectionUtils.newArrayList; |
| | | import static org.opends.server.util.ServerConstants.MBEAN_BASE_DOMAIN; |
| | | import static org.opends.server.util.StaticUtils.byteToHex; |
| | | import static org.opends.server.util.StaticUtils.isAlpha; |
| | | import static org.opends.server.util.StaticUtils.isDigit; |
| | | |
| | |
| | | { |
| | | try |
| | | { |
| | | String typeStr = null; |
| | | String dnString = configEntryDN.toString(); |
| | | if (dnString != null && dnString.length() != 0) |
| | | StringBuilder buffer = new StringBuilder(); |
| | | // Walk the RDNs from the root, rather than splitting the DN string at every comma. |
| | | for (int j = configEntryDN.size() - 1; j >= 0; j--) |
| | | { |
| | | StringBuilder buffer = new StringBuilder(dnString.length()); |
| | | String rdns[] = dnString.replace(',', ';').split(";"); |
| | | for (int j = rdns.length - 1; j >= 0; j--) |
| | | { |
| | | int rdnIndex = rdns.length - j; |
| | | buffer.append(",Rdn").append(rdnIndex).append("=") ; |
| | | for (int i = 0; i < rdns[j].length(); i++) |
| | | { |
| | | char c = rdns[j].charAt(i); |
| | | if (isAlpha(c) || isDigit(c)) |
| | | { |
| | | buffer.append(c); |
| | | } else |
| | | { |
| | | switch (c) |
| | | { |
| | | case ' ': |
| | | buffer.append("_"); |
| | | break; |
| | | case '=': |
| | | buffer.append("-"); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | typeStr = buffer.toString(); |
| | | int rdnIndex = configEntryDN.size() - j; |
| | | buffer.append(",Rdn").append(rdnIndex).append("="); |
| | | appendJmxRdn(buffer, configEntryDN.parent(j).rdn()); |
| | | } |
| | | |
| | | return MBEAN_BASE_DOMAIN + ":" + "Name=rootDSE" + typeStr; |
| | | // The root DN keeps the name it always had, which ends with "null". |
| | | return MBEAN_BASE_DOMAIN + ":" + "Name=rootDSE" + (buffer.length() != 0 ? buffer : "null"); |
| | | } catch (Exception e) |
| | | { |
| | | logger.traceException(e); |
| | |
| | | } |
| | | |
| | | /** |
| | | * Appends the JMX form of an RDN to the provided buffer. The attribute name keeps only its letters and |
| | | * digits, and is followed by '-' and the value. A value made of letters, digits and spaces is written as it |
| | | * always was, with '_' for a space. Any other value would lose characters that way, and two DNs would share |
| | | * a name, so it is percent-encoded instead: its letters and digits are kept, a space is still written as '_', |
| | | * and every other character, '_' included, is written as the '%' encoded bytes of its UTF-8 form. The SNMP |
| | | * extension relies on the '_': it finds the connection handlers, whose names hold an IP address, by |
| | | * "Connection_Handler" and their statistics by "_Statistics". The AVAs of a multi-valued RDN are joined |
| | | * with '+'. |
| | | */ |
| | | private static void appendJmxRdn(StringBuilder buffer, RDN rdn) |
| | | { |
| | | boolean first = true; |
| | | for (AVA ava : rdn) |
| | | { |
| | | if (!first) |
| | | { |
| | | buffer.append('+'); |
| | | } |
| | | first = false; |
| | | String name = ava.getAttributeName(); |
| | | for (int i = 0; i < name.length(); i++) |
| | | { |
| | | char c = name.charAt(i); |
| | | if (isAlpha(c) || isDigit(c)) |
| | | { |
| | | buffer.append(c); |
| | | } |
| | | } |
| | | buffer.append('-'); |
| | | String value = ava.getAttributeValue().toString(); |
| | | if (isPlainJmxValue(value)) |
| | | { |
| | | buffer.append(value.replace(' ', '_')); |
| | | } |
| | | else |
| | | { |
| | | for (byte b : ava.getAttributeValue().toByteArray()) |
| | | { |
| | | char c = (char) (b & 0xFF); |
| | | if (c == ' ') |
| | | { |
| | | buffer.append('_'); |
| | | } |
| | | else if (c < 0x80 && (isAlpha(c) || isDigit(c))) |
| | | { |
| | | buffer.append(c); |
| | | } |
| | | else |
| | | { |
| | | buffer.append('%').append(byteToHex(b)); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | private static boolean isPlainJmxValue(String value) |
| | | { |
| | | for (int i = 0; i < value.length(); i++) |
| | | { |
| | | char c = value.charAt(i); |
| | | if (!isAlpha(c) && !isDigit(c) && c != ' ') |
| | | { |
| | | return false; |
| | | } |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * Creates a new dynamic JMX MBean for use with the Directory Server. |
| | | * |
| | | * @param configEntryDN The DN of the configuration entry with which this |
| | |
| | | |
| | | import org.forgerock.http.routing.Router; |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.i18n.LocalizedIllegalArgumentException; |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.opendj.adapter.server3x.Converters; |
| | | import org.forgerock.opendj.config.ConfigurationFramework; |
| | |
| | | import org.forgerock.opendj.config.server.ServerManagementContext; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.opendj.ldap.SearchScope; |
| | | import org.forgerock.opendj.ldap.schema.Schema; |
| | | import org.forgerock.opendj.server.config.server.AlertHandlerCfg; |
| | | import org.forgerock.opendj.server.config.server.ConnectionHandlerCfg; |
| | |
| | | */ |
| | | public static DN getMonitorProviderDN(MonitorProvider<?> provider) |
| | | { |
| | | // Get a complete DN which could be a tree naming schema |
| | | return DN.valueOf("cn=" + provider.getMonitorInstanceName() + "," + DN_MONITOR_ROOT); |
| | | final String name = provider.getMonitorInstanceName(); |
| | | final DN monitorRoot = DN.valueOf(DN_MONITOR_ROOT); |
| | | // The name may be a relative DN that names an entry below cn=monitor, possibly several levels down: |
| | | // the replication monitors build a tree that way. |
| | | try |
| | | { |
| | | final DN dn = DN.valueOf("cn=" + name + "," + DN_MONITOR_ROOT); |
| | | if (dn.isInScopeOf(monitorRoot, SearchScope.SUBORDINATES)) |
| | | { |
| | | return dn; |
| | | } |
| | | } |
| | | catch (LocalizedIllegalArgumentException e) |
| | | { |
| | | // Not a relative DN, see below. |
| | | } |
| | | // Otherwise the whole name is the value of the RDN of the entry. A name that is not a relative DN, such as |
| | | // "LDAP, internal 0.0.0.0 port 1389", must not make the whole monitor backend fail. |
| | | return monitorRoot.child("cn", name); |
| | | } |
| | | |
| | | /** |
| | |
| | | |
| | | @Override |
| | | public String getMonitorInstanceName() { |
| | | return instanceName + "," + "cn=" + baseName; |
| | | return DN.escapeAttributeValue(instanceName) + "," + "cn=" + baseName; |
| | | } |
| | | |
| | | @Override |
| | |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.monitors; |
| | | |
| | |
| | | import java.util.Collection; |
| | | import java.util.TreeMap; |
| | | |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.config.server.ConfigException; |
| | | import org.forgerock.opendj.server.config.server.ClientConnectionMonitorProviderCfg; |
| | | import org.opends.server.api.ClientConnection; |
| | |
| | | { |
| | | // Client connections of a connection handler |
| | | return "Client Connections" + ",cn=" |
| | | + handler.getConnectionHandlerName(); |
| | | + DN.escapeAttributeValue(handler.getConnectionHandlerName()); |
| | | } |
| | | } |
| | | |
| | |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.monitors; |
| | | |
| | |
| | | import java.util.Collection; |
| | | import java.util.LinkedList; |
| | | |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.server.config.server.ConnectionHandlerCfg; |
| | | import org.forgerock.opendj.server.config.server.MonitorProviderCfg; |
| | | import org.opends.server.api.ClientConnection; |
| | |
| | | @Override |
| | | public void initializeMonitorProvider(MonitorProviderCfg configuration) |
| | | { |
| | | monitorName = connectionHandler.getConnectionHandlerName(); |
| | | // The name of the connection handler is the value of the RDN of the monitor entry. |
| | | monitorName = DN.escapeAttributeValue(connectionHandler.getConnectionHandlerName()); |
| | | } |
| | | |
| | | |
| | |
| | | * |
| | | * Copyright 2008-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.monitors; |
| | | |
| | | import static org.opends.messages.ConfigMessages.*; |
| | | |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.opendj.config.server.ConfigException; |
| | |
| | | String entryCacheName, |
| | | EntryCache<? extends EntryCacheCfg> entryCache) |
| | | { |
| | | this.entryCacheName = entryCacheName + " Entry Cache"; |
| | | this.entryCacheName = DN.escapeAttributeValue(entryCacheName) + " Entry Cache"; |
| | | this.entryCache = entryCache; |
| | | } |
| | | |
| | |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.monitors; |
| | | |
| | |
| | | @Override |
| | | public void initializeMonitorProvider(MonitorProviderCfg configuration) |
| | | { |
| | | monitorName = backend.getBackendID() + " Backend"; |
| | | monitorName = DN.escapeAttributeValue(backend.getBackendID()) + " Backend"; |
| | | } |
| | | |
| | | @Override |
| | |
| | | } |
| | | |
| | | // Create and register monitors. |
| | | statTracker = new HTTPStatistics(handlerName + " Statistics"); |
| | | statTracker = new HTTPStatistics(DN.escapeAttributeValue(handlerName) + " Statistics"); |
| | | DirectoryServer.registerMonitorProvider(statTracker); |
| | | |
| | | connMonitor = new ClientConnectionMonitorProvider(this); |
| | |
| | | } |
| | | |
| | | // Create and register monitors. |
| | | statTracker = new LDAPStatistics(handlerName + " Statistics"); |
| | | statTracker = new LDAPStatistics(DN.escapeAttributeValue(handlerName) + " Statistics"); |
| | | DirectoryServer.registerMonitorProvider(statTracker); |
| | | |
| | | connMonitor = new ClientConnectionMonitorProvider(this); |
| | |
| | | * |
| | | * Copyright 2006-2009 Sun Microsystems, Inc. |
| | | * Portions Copyright 2011-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.replication.common; |
| | | |
| | | import static org.opends.messages.ReplicationMessages.*; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.HashMap; |
| | | import java.util.Iterator; |
| | |
| | | try |
| | | { |
| | | // Split the provided multiDomainServerState into domains |
| | | String[] domains = multiDomainServerState.split(";"); |
| | | for (String domain : domains) |
| | | for (String domain : splitDomains(multiDomainServerState)) |
| | | { |
| | | // For each domain, split the CSNs by server |
| | | // and build a server state (SHOULD BE OPTIMIZED) |
| | | final ServerState serverStateByDomain = new ServerState(); |
| | | |
| | | final String[] fields = domain.split(":"); |
| | | if (fields.length == 0) |
| | | // The base DN may contain ':' (o=urn:x), but a CSN never does: the state starts after the last one. |
| | | final int colonPos = domain.lastIndexOf(':'); |
| | | final String domainBaseDN = colonPos >= 0 ? domain.substring(0, colonPos) : domain; |
| | | if (colonPos >= 0 && colonPos + 1 < domain.length()) |
| | | { |
| | | throw new DirectoryException(ResultCode.PROTOCOL_ERROR, |
| | | ERR_INVALID_COOKIE_SYNTAX.get(multiDomainServerState)); |
| | | } |
| | | final String domainBaseDN = fields[0]; |
| | | if (fields.length > 1) |
| | | { |
| | | final String serverStateStr = fields[1]; |
| | | final String serverStateStr = domain.substring(colonPos + 1); |
| | | for (String csnStr : serverStateStr.split(" ")) |
| | | { |
| | | final CSN csn = new CSN(csnStr); |
| | |
| | | startStates.put(DN.valueOf(domainBaseDN), serverStateByDomain); |
| | | } |
| | | } |
| | | catch (DirectoryException de) |
| | | { |
| | | throw de; |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | throw new DirectoryException( |
| | |
| | | } |
| | | return startStates; |
| | | } |
| | | |
| | | /** |
| | | * Splits the provided cookie at each ';' that ends a domain. {@link DN#toString()} escapes a ';' of the base DN |
| | | * as "\;", so a ';' preceded by an unescaped backslash belongs to the base DN. As {@link String#split(String)} |
| | | * did, empty trailing domains are dropped. |
| | | */ |
| | | private static List<String> splitDomains(String multiDomainServerState) |
| | | { |
| | | final List<String> domains = new ArrayList<>(); |
| | | int start = 0; |
| | | boolean escaped = false; |
| | | for (int i = 0; i < multiDomainServerState.length(); i++) |
| | | { |
| | | final char c = multiDomainServerState.charAt(i); |
| | | if (escaped) |
| | | { |
| | | escaped = false; |
| | | } |
| | | else if (c == '\\') |
| | | { |
| | | escaped = true; |
| | | } |
| | | else if (c == ';') |
| | | { |
| | | domains.add(multiDomainServerState.substring(start, i)); |
| | | start = i + 1; |
| | | } |
| | | } |
| | | domains.add(multiDomainServerState.substring(start)); |
| | | while (!domains.isEmpty() && domains.get(domains.size() - 1).isEmpty()) |
| | | { |
| | | domains.remove(domains.size() - 1); |
| | | } |
| | | return domains; |
| | | } |
| | | } |
| | |
| | | @Override |
| | | public String getMonitorInstanceName() |
| | | { |
| | | return "Replication server RS(" + localReplicationServer.getServerId() |
| | | + ") " + localReplicationServer.getServerURL() + ",cn=" |
| | | + baseDN.toString().replace(',', '_').replace('=', '_') |
| | | return getMonitorInstanceName(localReplicationServer.getServerId(), localReplicationServer.getServerURL(), baseDN); |
| | | } |
| | | |
| | | /** |
| | | * Returns the name of the monitor of a replication server domain: the relative DN of its entry below cn=monitor. |
| | | * |
| | | * @param serverId |
| | | * the server ID of the replication server |
| | | * @param serverURL |
| | | * the URL of the replication server |
| | | * @param baseDN |
| | | * the base DN of the domain |
| | | * @return the name of the monitor of the replication server domain |
| | | */ |
| | | static String getMonitorInstanceName(int serverId, String serverURL, DN baseDN) |
| | | { |
| | | // The base DN, with '_' for ',' and '=', is the value of an RDN: escape what is left, such as '+'. |
| | | return "Replication server RS(" + serverId + ") " + serverURL |
| | | + ",cn=" + DN.escapeAttributeValue(baseDN.toString().replace(',', '_').replace('=', '_')) |
| | | + ",cn=Replication"; |
| | | } |
| | | |
| | |
| | | * |
| | | * Copyright 2006-2010 Sun Microsystems, Inc. |
| | | * Portions copyright 2013-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.replication.service; |
| | | |
| | |
| | | import java.util.Map.Entry; |
| | | |
| | | import org.opends.server.api.MonitorData; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.server.config.server.MonitorProviderCfg; |
| | | import org.opends.server.api.MonitorProvider; |
| | | import org.opends.server.types.HostPort; |
| | | import org.opends.server.replication.service.ReplicationDomain.ImportExportContext; |
| | | |
| | | /** |
| | |
| | | @Override |
| | | public String getMonitorInstanceName() |
| | | { |
| | | return "Directory server DS(" + domain.getServerId() + ") " |
| | | + domain.getLocalUrl() |
| | | + ",cn=" + domain.getBaseDN().toString().replace(',', '_').replace('=', '_') |
| | | return getMonitorInstanceName(domain.getServerId(), domain.getLocalUrl(), domain.getBaseDN()); |
| | | } |
| | | |
| | | /** |
| | | * Returns the name of the monitor of a replication domain: the relative DN of its entry below cn=monitor. |
| | | * |
| | | * @param serverId |
| | | * the server ID of the replication domain |
| | | * @param localUrl |
| | | * the URL of the replication domain |
| | | * @param baseDN |
| | | * the base DN of the replication domain |
| | | * @return the name of the monitor of the replication domain |
| | | */ |
| | | static String getMonitorInstanceName(int serverId, HostPort localUrl, DN baseDN) |
| | | { |
| | | // The base DN, with '_' for ',' and '=', is the value of an RDN: escape what is left, such as '+'. |
| | | return "Directory server DS(" + serverId + ") " + localUrl |
| | | + ",cn=" + DN.escapeAttributeValue(baseDN.toString().replace(',', '_').replace('=', '_')) |
| | | + ",cn=Replication"; |
| | | } |
| | | |
| | |
| | | * |
| | | * Copyright 2006-2008 Sun Microsystems, Inc. |
| | | * Portions Copyright 2012-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.tools; |
| | | |
| | |
| | | public static int mainVerifyIndex(String[] args, boolean initializeServer, |
| | | OutputStream errStream) |
| | | { |
| | | final long result = verifyIndex(args, initializeServer, errStream, false); |
| | | return result == VERIFY_FAILED ? 1 : (int) Math.min(result, Integer.MAX_VALUE); |
| | | } |
| | | |
| | | /** |
| | | * Verifies the indexes as {@link #mainVerifyIndex} does with {@code --countErrors}, but tells a verification that |
| | | * could not be performed apart from one that found errors: the tool exits with 1 in both cases. |
| | | * |
| | | * @param args The command-line arguments provided to this |
| | | * program. |
| | | * @param initializeServer Indicates whether to initialize the server. |
| | | * @param errStream The output stream to use for standard error, or |
| | | * {@code null} if standard error is not needed. |
| | | * @return The number of errors found in the indexes, or {@code -1} if they could not be verified. |
| | | */ |
| | | public static long countIndexErrors(String[] args, boolean initializeServer, OutputStream errStream) |
| | | { |
| | | return verifyIndex(args, initializeServer, errStream, true); |
| | | } |
| | | |
| | | /** What {@link #verifyIndex} returns when the indexes could not be verified. */ |
| | | private static final long VERIFY_FAILED = -1; |
| | | |
| | | /** |
| | | * Returns the number of errors found when {@code --countErrors} is present or {@code countErrorsAlways} is |
| | | * {@code true}, else 0, or {@link #VERIFY_FAILED} if the indexes could not be verified. |
| | | */ |
| | | private static long verifyIndex(String[] args, boolean initializeServer, OutputStream errStream, |
| | | boolean countErrorsAlways) |
| | | { |
| | | PrintStream err = NullOutputStream.wrapOrNullStream(errStream); |
| | | JDKLogging.enableConsoleLoggingForOpenDJTool(); |
| | | |
| | |
| | | catch (ArgumentException ae) |
| | | { |
| | | printWrappedText(err, ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage())); |
| | | return 1; |
| | | return VERIFY_FAILED; |
| | | } |
| | | |
| | | |
| | |
| | | catch (ArgumentException ae) |
| | | { |
| | | argParser.displayMessageAndUsageReference(err, ERR_ERROR_PARSING_ARGS.get(ae.getMessage())); |
| | | return 1; |
| | | return VERIFY_FAILED; |
| | | } |
| | | |
| | | |
| | |
| | | if (cleanMode.isPresent() && indexList.getValues().size() != 1) |
| | | { |
| | | argParser.displayMessageAndUsageReference(err, ERR_VERIFYINDEX_VERIFY_CLEAN_REQUIRES_SINGLE_INDEX.get()); |
| | | return 1; |
| | | return VERIFY_FAILED; |
| | | } |
| | | |
| | | // Checks the version - if upgrade required, the tool is unusable |
| | |
| | | catch (InitializationException e) |
| | | { |
| | | printWrappedText(err, e.getMessage()); |
| | | return 1; |
| | | return VERIFY_FAILED; |
| | | } |
| | | |
| | | if (initializeServer) |
| | |
| | | catch (InitializationException ie) |
| | | { |
| | | printWrappedText(err, ERR_CANNOT_INITIALIZE_SERVER_COMPONENTS.get(ie.getLocalizedMessage())); |
| | | return 1; |
| | | return VERIFY_FAILED; |
| | | } |
| | | } |
| | | |
| | |
| | | catch (Exception e) |
| | | { |
| | | printWrappedText(err, ERR_CANNOT_DECODE_BASE_DN.get(baseDNString.getValue(), getExceptionMessage(e))); |
| | | return 1; |
| | | return VERIFY_FAILED; |
| | | } |
| | | |
| | | |
| | |
| | | if (backend != null) |
| | | { |
| | | printWrappedText(err, ERR_MULTIPLE_BACKENDS_FOR_BASE.get(baseDNString.getValue())); |
| | | return 1; |
| | | return VERIFY_FAILED; |
| | | } |
| | | backend = b; |
| | | } |
| | |
| | | if (backend == null) |
| | | { |
| | | printWrappedText(err, ERR_NO_BACKENDS_FOR_BASE.get(baseDNString.getValue())); |
| | | return 1; |
| | | return VERIFY_FAILED; |
| | | } |
| | | |
| | | if (!backend.supports(BackendOperation.INDEXING)) |
| | | { |
| | | printWrappedText(err, ERR_BACKEND_NO_INDEXING_SUPPORT.get()); |
| | | return 1; |
| | | return VERIFY_FAILED; |
| | | } |
| | | |
| | | // Initialize the verify configuration. |
| | |
| | | if (! LockFileManager.acquireSharedLock(lockFile, failureReason)) |
| | | { |
| | | printWrappedText(err, ERR_VERIFYINDEX_CANNOT_LOCK_BACKEND.get(backend.getBackendID(), failureReason)); |
| | | return 1; |
| | | return VERIFY_FAILED; |
| | | } |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | printWrappedText(err, ERR_VERIFYINDEX_CANNOT_LOCK_BACKEND.get(backend.getBackendID(), getExceptionMessage(e))); |
| | | return 1; |
| | | return VERIFY_FAILED; |
| | | } |
| | | |
| | | |
| | |
| | | { |
| | | // Launch the verify process. |
| | | final long errorCount = backend.verifyBackend(verifyConfig); |
| | | if (countErrors.isPresent()) |
| | | { |
| | | if (errorCount > Integer.MAX_VALUE) |
| | | { |
| | | return Integer.MAX_VALUE; |
| | | } |
| | | return (int) errorCount; |
| | | } |
| | | return 0; |
| | | return countErrors.isPresent() || countErrorsAlways ? errorCount : 0; |
| | | } |
| | | catch (InitializationException e) |
| | | { |
| | | printWrappedText(err, ERR_VERIFYINDEX_ERROR_DURING_VERIFY.get(e.getMessage())); |
| | | return 1; |
| | | return VERIFY_FAILED; |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | printWrappedText(err, ERR_VERIFYINDEX_ERROR_DURING_VERIFY.get(stackTraceToSingleLineString(e))); |
| | | return 1; |
| | | return VERIFY_FAILED; |
| | | } |
| | | finally |
| | | { |
| | |
| | | * |
| | | * Copyright 2009-2010 Sun Microsystems, Inc. |
| | | * Portions Copyright 2014-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.tools.tasks; |
| | | |
| | |
| | | |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.DecodeException; |
| | | import org.forgerock.opendj.ldap.DereferenceAliasesPolicy; |
| | | import org.forgerock.opendj.ldap.ModificationType; |
| | |
| | | RawAttribute recurringIDAttr = getAttribute(ATTR_RECURRING_TASK_ID, |
| | | taskAttributes); |
| | | |
| | | // The task ID may be typed by the user (--recurringTask uses the --backupID): escape it. |
| | | if (recurringIDAttr != null) { |
| | | entryDN = ATTR_RECURRING_TASK_ID + "=" + |
| | | taskID + "," + RECURRING_TASK_BASE_RDN + "," + DN_TASK_ROOT; |
| | | entryDN = DN.valueOf(RECURRING_TASK_BASE_RDN + "," + DN_TASK_ROOT) |
| | | .child(ATTR_RECURRING_TASK_ID, taskID).toString(); |
| | | } else { |
| | | entryDN = ATTR_TASK_ID + "=" + taskID + "," + |
| | | SCHEDULED_TASK_BASE_RDN + "," + DN_TASK_ROOT; |
| | | entryDN = DN.valueOf(SCHEDULED_TASK_BASE_RDN + "," + DN_TASK_ROOT) |
| | | .child(ATTR_TASK_ID, taskID).toString(); |
| | | } |
| | | return entryDN; |
| | | } |
| | |
| | | REFERENTIAL_INTEGRITY_PLUGIN_FILTER, |
| | | ADD_REFERENTIAL_INTEGRITY_PRE_OPERATION_PLUGIN_TYPES)); |
| | | |
| | | /* See issue #1153: a DN is now read up to its end, ';' separating RDNs as ',' does, so a DN value that a client |
| | | * wrote with ';' gets another equality key than the one a previous version indexed. The value is found by |
| | | * reading the entries only, so the equality indexes of the attributes holding DNs are verified, and rebuilt |
| | | * where they miss a key, rather than rebuilt on every server. */ |
| | | register("5.2.0", |
| | | verifyAndRebuildDNEqualityIndexes(INFO_UPGRADE_VERIFY_DN_EQUALITY_INDEXES.get())); |
| | | |
| | | /* |
| | | * See issue #746. Builds before #661 (fixed in 5.1.2) shipped a duplicate |
| | | * org.openidentityplatform.opendj.opendj-server-legacy.jar alongside opendj.jar in lib/. |
| | |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Portions Copyright 2013-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.tools.upgrade; |
| | | |
| | |
| | | import static javax.security.auth.callback.TextOutputCallback.*; |
| | | import static org.forgerock.util.Utils.joinAsString; |
| | | import static org.opends.messages.ToolMessages.*; |
| | | import static org.opends.server.schema.SchemaConstants.EMR_DN_NAME; |
| | | import static org.opends.server.tools.upgrade.FileManager.copyRecursively; |
| | | import static org.opends.server.tools.upgrade.UpgradeUtils.*; |
| | | import static org.opends.server.util.StaticUtils.*; |
| | |
| | | import java.io.File; |
| | | import java.io.FileReader; |
| | | import java.io.IOException; |
| | | import java.io.PrintStream; |
| | | import java.nio.file.Files; |
| | | import java.util.ArrayList; |
| | | import java.util.Arrays; |
| | | import java.util.Collection; |
| | | import java.util.Collections; |
| | | import java.util.HashSet; |
| | | import java.util.LinkedHashSet; |
| | |
| | | import org.forgerock.opendj.ldif.LDIFEntryReader; |
| | | import org.opends.server.backends.pluggable.spi.TreeName; |
| | | import org.opends.server.tools.RebuildIndex; |
| | | import org.opends.server.tools.VerifyIndex; |
| | | import org.opends.server.util.BuildVersion; |
| | | import org.opends.server.util.ChangeOperationType; |
| | | import org.opends.server.util.StaticUtils; |
| | |
| | | } |
| | | |
| | | /** |
| | | * Creates a task that verifies, at the end of the upgrade, the equality indexes of the attributes whose values |
| | | * are compared as DNs, and rebuilds them under each base DN where they do not match the entries. Only an index |
| | | * that is missing a key needs the rebuild: the verification computes the keys of every entry with the matching |
| | | * rules of the upgraded server, and finds those that were computed differently, or not at all, by the previous |
| | | * version. Unlike {@link #rebuildIndexesNamed}, a backend whose indexes match its entries is only read. |
| | | * |
| | | * @param summary |
| | | * A message describing why the indexes are verified and asking whether to do it at the end of the |
| | | * upgrade. |
| | | * @return The verify and rebuild task. |
| | | */ |
| | | static UpgradeTask verifyAndRebuildDNEqualityIndexes(final LocalizableMessage summary) |
| | | { |
| | | return new AbstractUpgradeTask() |
| | | { |
| | | private boolean isATaskToPerform; |
| | | |
| | | @Override |
| | | public void prepare(UpgradeContext context) throws ClientException |
| | | { |
| | | Upgrade.needToRunPostUpgradePhase(); |
| | | // Requires answer from the user. Verifying only reads, so it is done unless the user declines. |
| | | isATaskToPerform = context.confirmYN(summary, YES) == YES; |
| | | } |
| | | |
| | | @Override |
| | | public void postUpgrade(final UpgradeContext context) throws ClientException |
| | | { |
| | | if (!isATaskToPerform) |
| | | { |
| | | postponePostUpgrade(context); |
| | | return; |
| | | } |
| | | if (isRebuildAllIndexesTaskAccepted || indexesToRebuild.contains("." + EMR_DN_NAME)) |
| | | { |
| | | // These indexes are rebuilt at the end of the upgrade anyway. |
| | | return; |
| | | } |
| | | |
| | | final Map<String, Set<String>> attributesPerBackend; |
| | | try |
| | | { |
| | | attributesPerBackend = getDNEqualityIndexedAttributesPerBackend(); |
| | | } |
| | | catch (IOException e) |
| | | { |
| | | throw new ClientException(ReturnCode.ERROR_UNEXPECTED, ERR_UPGRADE_READING_CONF_FILE.get(e.getMessage()), e); |
| | | } |
| | | verifyAndRebuildOrWarn(context, configFile.getAbsolutePath(), |
| | | getDNEqualityIndexesToVerify(attributesPerBackend, getBaseDNsPerBackendsFromConfig()), true, |
| | | UpgradeLog::getPrintStream); |
| | | } |
| | | |
| | | @Override |
| | | public void postponePostUpgrade(UpgradeContext context) throws ClientException |
| | | { |
| | | if (!isRebuildAllIndexesIsPresent) |
| | | { |
| | | context.notify(INFO_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_DECLINED.get(), TextOutputCallback.WARNING); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public String toString() |
| | | { |
| | | return String.valueOf(summary); |
| | | } |
| | | }; |
| | | } |
| | | |
| | | /** |
| | | * Returns the base DNs whose DN equality indexes are verified, with the attributes of these indexes: those of the |
| | | * backend holding each base DN. A backend whose base DNs are unknown is left out. |
| | | * |
| | | * @param attributesPerBackend |
| | | * The attributes with a DN equality index, per backend ID. |
| | | * @param baseDNsPerBackend |
| | | * The base DNs, per backend ID. |
| | | * @return The attributes whose indexes are verified, per base DN. |
| | | */ |
| | | static Map<String, Set<String>> getDNEqualityIndexesToVerify(final Map<String, Set<String>> attributesPerBackend, |
| | | final Map<String, Set<String>> baseDNsPerBackend) |
| | | { |
| | | final Map<String, Set<String>> attributesPerBaseDN = new TreeMap<>(); |
| | | for (final Map.Entry<String, Set<String>> backend : attributesPerBackend.entrySet()) |
| | | { |
| | | final Set<String> baseDNs = baseDNsPerBackend.get(backend.getKey()); |
| | | if (baseDNs != null) |
| | | { |
| | | for (final String baseDN : baseDNs) |
| | | { |
| | | attributesPerBaseDN.put(baseDN, backend.getValue()); |
| | | } |
| | | } |
| | | } |
| | | return attributesPerBaseDN; |
| | | } |
| | | |
| | | /** Opens the stream the tools write to for a base DN: the rebuild closes it. */ |
| | | interface ToolOutput |
| | | { |
| | | /** |
| | | * Opens the stream. |
| | | * |
| | | * @return The stream the tools write to. |
| | | * @throws ClientException |
| | | * If the stream cannot be opened. |
| | | */ |
| | | PrintStream open() throws ClientException; |
| | | } |
| | | |
| | | /** |
| | | * Verifies the DN equality indexes under each base DN, and rebuilds them if needed, as |
| | | * {@link #verifyAndRebuildOrWarn(UpgradeContext, String, String, Set, boolean, PrintStream)} does for one base DN. |
| | | * Once a rebuild has failed, the indexes of the next base DNs are neither verified nor rebuilt, and the user is |
| | | * told which ones were left: the cause of the failure, such as a full temporary directory, would most likely make |
| | | * their rebuild fail too, after it had deleted them. |
| | | * |
| | | * @param context |
| | | * The upgrade context, which is notified of the outcome. |
| | | * @param configFilePath |
| | | * The path of the configuration file. |
| | | * @param indexesPerBaseDN |
| | | * The attributes whose indexes are verified, and rebuilt if needed, per base DN. |
| | | * @param initializeServer |
| | | * Whether the tools have to initialize the server components. |
| | | * @param output |
| | | * Opens the stream the tools write to, once per base DN. |
| | | * @throws ClientException |
| | | * If a rebuild fails, or if the outcome cannot be reported. |
| | | */ |
| | | static void verifyAndRebuildOrWarn(final UpgradeContext context, final String configFilePath, |
| | | final Map<String, Set<String>> indexesPerBaseDN, final boolean initializeServer, final ToolOutput output) |
| | | throws ClientException |
| | | { |
| | | ClientException failedRebuild = null; |
| | | for (final Map.Entry<String, Set<String>> baseDN : indexesPerBaseDN.entrySet()) |
| | | { |
| | | if (failedRebuild != null) |
| | | { |
| | | context.notify(WARN_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_SKIPPED.get( |
| | | joinAsString(", ", baseDN.getValue()), baseDN.getKey()), WARNING); |
| | | continue; |
| | | } |
| | | final PrintStream out = output.open(); |
| | | try |
| | | { |
| | | verifyAndRebuildOrWarn(context, configFilePath, baseDN.getKey(), baseDN.getValue(), initializeServer, out); |
| | | } |
| | | catch (final ClientException e) |
| | | { |
| | | failedRebuild = e; |
| | | } |
| | | finally |
| | | { |
| | | // A rebuild closes it too: closing it again does nothing |
| | | close(out); |
| | | } |
| | | } |
| | | if (failedRebuild != null) |
| | | { |
| | | throw failedRebuild; |
| | | } |
| | | } |
| | | |
| | | /** What {@link #verifyAndRebuildIndexes} found. */ |
| | | enum IndexVerification |
| | | { |
| | | /** The indexes match the entries. */ |
| | | CONSISTENT, |
| | | /** The indexes did not match the entries, and were rebuilt. */ |
| | | REBUILT, |
| | | /** The indexes could not be verified, as when their backend cannot be read: they were left as they were. */ |
| | | NOT_VERIFIED |
| | | } |
| | | |
| | | /** |
| | | * Verifies the provided DN equality indexes under a base DN, and rebuilds them if needed, as |
| | | * {@link #verifyAndRebuildIndexes} does, then tells the user the outcome. When the indexes cannot be verified, the |
| | | * user is warned instead of the upgrade failing: they were left as they were, and a backend that cannot be read |
| | | * now, such as a JDBC or Cassandra backend whose database is down, is left to the administrator. A rebuild that |
| | | * fails still fails the upgrade, as it may have left the indexes untrusted. |
| | | * |
| | | * @param context |
| | | * The upgrade context, which is notified of the outcome. |
| | | * @param configFilePath |
| | | * The path of the configuration file. |
| | | * @param baseDN |
| | | * The base DN to verify. |
| | | * @param attributes |
| | | * The attributes whose indexes are verified, and rebuilt if needed. |
| | | * @param initializeServer |
| | | * Whether the tools have to initialize the server components. |
| | | * @param out |
| | | * The stream the tools write to. |
| | | * @throws ClientException |
| | | * If the rebuild fails, or if the outcome cannot be reported. |
| | | */ |
| | | static void verifyAndRebuildOrWarn(final UpgradeContext context, final String configFilePath, final String baseDN, |
| | | final Set<String> attributes, final boolean initializeServer, final PrintStream out) throws ClientException |
| | | { |
| | | final String indexes = joinAsString(", ", attributes); |
| | | final ProgressNotificationCallback pnc = new ProgressNotificationCallback( |
| | | INFORMATION, INFO_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_STARTS.get(indexes, baseDN), 25); |
| | | context.notifyProgress(pnc); |
| | | final IndexVerification verification; |
| | | try |
| | | { |
| | | verification = verifyAndRebuildIndexes(configFilePath, baseDN, attributes, initializeServer, out); |
| | | } |
| | | catch (final ClientException e) |
| | | { |
| | | context.notifyProgress(pnc.setProgress(-100)); |
| | | throw e; |
| | | } |
| | | context.notifyProgress(pnc.setProgress(100)); |
| | | switch (verification) |
| | | { |
| | | case CONSISTENT: |
| | | context.notify(INFO_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_CONSISTENT.get(indexes, baseDN)); |
| | | break; |
| | | case REBUILT: |
| | | context.notify(INFO_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_INCONSISTENT.get(indexes, baseDN)); |
| | | break; |
| | | default: |
| | | context.notify(WARN_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_FAILED.get(indexes, baseDN), WARNING); |
| | | break; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Verifies the provided indexes under a base DN with the verify-index tool, and rebuilds them with the |
| | | * rebuild-index tool when it reports any error. Indexes that cannot be verified are not rebuilt: whatever keeps |
| | | * them from being read would most likely make the rebuild fail too, after it has deleted them. The backend must |
| | | * not be in use by a running server. |
| | | * |
| | | * @param configFilePath |
| | | * The path of the configuration file. |
| | | * @param baseDN |
| | | * The base DN to verify. |
| | | * @param attributes |
| | | * The attributes whose indexes are verified, and rebuilt if needed. |
| | | * @param initializeServer |
| | | * Whether the tools have to initialize the server components. |
| | | * @param out |
| | | * The stream the tools write to. |
| | | * @return What the verification found. |
| | | * @throws ClientException |
| | | * If the rebuild fails. |
| | | */ |
| | | static IndexVerification verifyAndRebuildIndexes(final String configFilePath, final String baseDN, |
| | | final Collection<String> attributes, final boolean initializeServer, final PrintStream out) |
| | | throws ClientException |
| | | { |
| | | final List<String> args = new ArrayList<>(); |
| | | args.add("--configFile"); |
| | | args.add(configFilePath); |
| | | args.add("--baseDN"); |
| | | args.add(baseDN); |
| | | for (final String attribute : attributes) |
| | | { |
| | | args.add("--index"); |
| | | args.add(attribute); |
| | | } |
| | | |
| | | logger.debug(INFO_UPGRADE_REBUILD_INDEX_ARGUMENTS, args); |
| | | final long errors = VerifyIndex.countIndexErrors(args.toArray(new String[0]), initializeServer, out); |
| | | if (errors < 0) |
| | | { |
| | | return IndexVerification.NOT_VERIFIED; |
| | | } |
| | | if (errors == 0) |
| | | { |
| | | return IndexVerification.CONSISTENT; |
| | | } |
| | | |
| | | if (new RebuildIndex().rebuildIndexesWithinMultipleBackends(initializeServer, out, args) != 0) |
| | | { |
| | | throw new ClientException(ReturnCode.ERROR_UNEXPECTED, ERR_UPGRADE_PERFORMING_POST_TASKS_FAIL.get()); |
| | | } |
| | | return IndexVerification.REBUILT; |
| | | } |
| | | |
| | | /** |
| | | * This task is processed at the end of the upgrade, rebuilding indexes. If a |
| | | * rebuild all indexes has been registered before, it takes the flag |
| | | * relatively to single rebuild index. |
| | |
| | | import java.io.FilenameFilter; |
| | | import java.io.IOException; |
| | | import java.nio.file.Files; |
| | | import java.util.Arrays; |
| | | import java.util.HashMap; |
| | | import java.util.HashSet; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | import java.util.TreeMap; |
| | | import java.util.TreeSet; |
| | | |
| | | import org.forgerock.i18n.LocalizableMessage; |
| | | import org.forgerock.i18n.slf4j.LocalizedLogger; |
| | | import org.forgerock.opendj.ldap.Assertion; |
| | | import org.forgerock.opendj.ldap.AVA; |
| | | import org.forgerock.opendj.ldap.Attribute; |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.ConditionResult; |
| | |
| | | import org.forgerock.opendj.ldap.requests.ModifyRequest; |
| | | import org.forgerock.opendj.ldap.requests.Requests; |
| | | import org.forgerock.opendj.ldap.requests.SearchRequest; |
| | | import org.forgerock.opendj.ldap.schema.AttributeType; |
| | | import org.forgerock.opendj.ldap.schema.CoreSchema; |
| | | import org.forgerock.opendj.ldap.schema.MatchingRule; |
| | | import org.forgerock.opendj.ldap.schema.Schema; |
| | |
| | | import static org.forgerock.opendj.ldap.schema.SchemaOptions.*; |
| | | import static org.opends.messages.ConfigMessages.*; |
| | | import static org.opends.messages.ToolMessages.*; |
| | | import static org.opends.server.schema.SchemaConstants.EMR_DN_OID; |
| | | import static org.opends.server.schema.SchemaConstants.EMR_UNIQUE_MEMBER_OID; |
| | | import static org.opends.server.tools.upgrade.FileManager.*; |
| | | import static org.opends.server.tools.upgrade.Installation.*; |
| | | import static org.opends.server.util.ChangeOperationType.*; |
| | |
| | | return baseDNs; |
| | | } |
| | | |
| | | /** |
| | | * Returns, for each enabled pluggable backend of the server, the attributes with an equality index whose matching |
| | | * rule compares DNs: "distinguishedNameMatch" (member, owner, seeAlso...) or "uniqueMemberMatch" (uniqueMember). |
| | | * Backends without such an index are left out. |
| | | * |
| | | * @return The names of these attributes, per backend ID. |
| | | * @throws IOException |
| | | * If the configuration or a schema file cannot be read. |
| | | */ |
| | | static Map<String, Set<String>> getDNEqualityIndexedAttributesPerBackend() throws IOException |
| | | { |
| | | return getDNEqualityIndexedAttributesPerBackend(configFile, configSchemaDirectory); |
| | | } |
| | | |
| | | /** |
| | | * Returns, for each enabled pluggable backend in the provided configuration, the attributes with an equality index |
| | | * whose matching rule compares DNs, as {@link #getDNEqualityIndexedAttributesPerBackend()} does. The matching rule |
| | | * of an attribute is read from the schema files of the instance, so that an attribute of a custom schema is found |
| | | * too. |
| | | * |
| | | * @param config |
| | | * The configuration file. |
| | | * @param schemaDirectory |
| | | * The directory of the schema files. |
| | | * @return The names of these attributes, per backend ID. |
| | | * @throws IOException |
| | | * If the configuration or a schema file cannot be read. |
| | | */ |
| | | static Map<String, Set<String>> getDNEqualityIndexedAttributesPerBackend(final File config, |
| | | final File schemaDirectory) throws IOException |
| | | { |
| | | final Schema schema = readSchemaFiles(schemaDirectory); |
| | | final Set<String> enabledBackends = new HashSet<>(); |
| | | final Map<String, Set<String>> attributes = new TreeMap<>(); |
| | | try (LDIFEntryReader reader = new LDIFEntryReader(new FileInputStream(config))) |
| | | { |
| | | while (reader.hasNext()) |
| | | { |
| | | final Entry entry = reader.readEntry(); |
| | | if (hasObjectClass(entry, "ds-cfg-pluggable-backend") |
| | | && "true".equalsIgnoreCase(entry.parseAttribute("ds-cfg-enabled").asString())) |
| | | { |
| | | enabledBackends.add(entry.parseAttribute("ds-cfg-backend-id").asString()); |
| | | } |
| | | else if (hasObjectClass(entry, "ds-cfg-backend-index") |
| | | && hasValueIgnoringCase(entry, "ds-cfg-index-type", "equality")) |
| | | { |
| | | final String attribute = entry.parseAttribute("ds-cfg-attribute").asString(); |
| | | final String backendID = getBackendID(entry.getName()); |
| | | if (attribute != null && backendID != null && comparesDNs(schema.getAttributeType(attribute))) |
| | | { |
| | | attributes.computeIfAbsent(backendID, id -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER)).add(attribute); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | attributes.keySet().retainAll(enabledBackends); |
| | | return attributes; |
| | | } |
| | | |
| | | private static Schema readSchemaFiles(final File schemaDirectory) throws IOException |
| | | { |
| | | final SchemaBuilder builder = new SchemaBuilder(Schema.getCoreSchema()); |
| | | final File[] files = schemaFilesInReadOrder(schemaDirectory); |
| | | if (files != null) |
| | | { |
| | | for (final File file : files) |
| | | { |
| | | try (LDIFEntryReader reader = new LDIFEntryReader(new FileInputStream(file))) |
| | | { |
| | | while (reader.hasNext()) |
| | | { |
| | | builder.addSchema(reader.readEntry(), true); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return builder.toSchema().asNonStrictSchema(); |
| | | } |
| | | |
| | | /** |
| | | * Returns the LDIF files of a schema directory in the order the server reads them, so that a later file may |
| | | * redefine an attribute of an earlier one. |
| | | * |
| | | * @param schemaDirectory |
| | | * The schema directory. |
| | | * @return The LDIF files, or {@code null} if the directory cannot be listed. |
| | | */ |
| | | static File[] schemaFilesInReadOrder(final File schemaDirectory) |
| | | { |
| | | final File[] files = schemaDirectory.listFiles((dir, name) -> name.toLowerCase().endsWith(".ldif")); |
| | | if (files != null) |
| | | { |
| | | Arrays.sort(files); |
| | | } |
| | | return files; |
| | | } |
| | | |
| | | private static boolean comparesDNs(final AttributeType attributeType) |
| | | { |
| | | final MatchingRule equality = attributeType.getEqualityMatchingRule(); |
| | | return equality != null |
| | | && (EMR_DN_OID.equals(equality.getOID()) || EMR_UNIQUE_MEMBER_OID.equals(equality.getOID())); |
| | | } |
| | | |
| | | private static boolean hasObjectClass(final Entry entry, final String objectClass) |
| | | { |
| | | return hasValueIgnoringCase(entry, "objectClass", objectClass); |
| | | } |
| | | |
| | | private static boolean hasValueIgnoringCase(final Entry entry, final String attribute, final String value) |
| | | { |
| | | for (final String v : entry.parseAttribute(attribute).asSetOfString()) |
| | | { |
| | | if (value.equalsIgnoreCase(v)) |
| | | { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | /** Returns the ID of the backend whose configuration entry is, or is above, the provided DN. */ |
| | | private static String getBackendID(final DN dn) |
| | | { |
| | | for (DN d = dn; d != null && !d.isRootDN(); d = d.parent()) |
| | | { |
| | | final AVA ava = d.rdn().getFirstAVA(); |
| | | if ("ds-cfg-backend-id".equalsIgnoreCase(ava.getAttributeName())) |
| | | { |
| | | return ava.getAttributeValue().toString(); |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | static EntryReader searchConfigFile(final SearchRequest searchRequest) throws FileNotFoundException |
| | | { |
| | | final Schema schema = getUpgradeSchema(); |
| | |
| | | */ |
| | | package org.opends.server.types; |
| | | |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.util.Iterator; |
| | | import java.util.LinkedHashSet; |
| | | import java.util.LinkedList; |
| | |
| | | continue; |
| | | } |
| | | |
| | | if (Character.isHighSurrogate(c) && i + 1 < length && Character.isLowSurrogate(s.charAt(i + 1))) |
| | | { |
| | | // A character outside the BMP: encode both chars of the surrogate pair as one code point. |
| | | hexEncode(s.substring(i, i + 2), buffer); |
| | | i++; |
| | | continue; |
| | | } |
| | | |
| | | if (c == ',') |
| | | { |
| | | if (isExtension) |
| | | { |
| | | hexEncode(c, buffer); |
| | | hexEncode(String.valueOf(c), buffer); |
| | | } |
| | | else |
| | | { |
| | |
| | | buffer.append(c); |
| | | break; |
| | | default: |
| | | hexEncode(c, buffer); |
| | | hexEncode(String.valueOf(c), buffer); |
| | | break; |
| | | } |
| | | } |
| | |
| | | |
| | | |
| | | /** |
| | | * Appends a percent-encoded representation of the provided |
| | | * character to the given buffer. |
| | | * Appends the percent-encoded UTF-8 octets of the provided |
| | | * characters to the given buffer, as RFC 4516 section 2.1 requires. |
| | | * |
| | | * @param c The character to add to the buffer. |
| | | * @param chars The characters to add to the buffer. |
| | | * @param buffer The buffer to which the percent-encoded |
| | | * representation should be written. |
| | | */ |
| | | private static void hexEncode(char c, StringBuilder buffer) |
| | | private static void hexEncode(String chars, StringBuilder buffer) |
| | | { |
| | | if ((c & (byte) 0xFF) == c) |
| | | for (byte b : chars.getBytes(StandardCharsets.UTF_8)) |
| | | { |
| | | // It's a single byte. |
| | | buffer.append('%'); |
| | | buffer.append(byteToHex((byte) c)); |
| | | } |
| | | else |
| | | { |
| | | // It requires two bytes, and each should be prefixed by a |
| | | // percent sign. |
| | | buffer.append('%'); |
| | | byte b1 = (byte) ((c >>> 8) & 0xFF); |
| | | buffer.append(byteToHex(b1)); |
| | | |
| | | buffer.append('%'); |
| | | byte b2 = (byte) (c & 0xFF); |
| | | buffer.append(byteToHex(b2)); |
| | | buffer.append(byteToHex(b)); |
| | | } |
| | | } |
| | | |
| | |
| | | extended operation handlers (RFC 5805) |
| | | INFO_UPGRADE_TASK_ADD_REFERENTIAL_INTEGRITY_PRE_OPERATION_PLUGIN_TYPES=Adding the pre-operation add \ |
| | | and modify plugin types to the referential integrity plugins |
| | | INFO_UPGRADE_VERIFY_DN_EQUALITY_INDEXES=OpenDJ 5.2.0 reads a DN up to its end. A DN value that a \ |
| | | client wrote with ';' between RDNs, or that was stored while its syntax was not enforced, may lack \ |
| | | its key in the equality indexes of the attributes holding DNs, like "member", "owner", "seeAlso" and \ |
| | | "uniqueMember", so that an indexed search misses its entry. Do you want to verify these indexes at \ |
| | | the end of the upgrade, and rebuild them under each base DN where they do not match the entries? \ |
| | | The verification reads every entry of the backends that have such indexes |
| | | INFO_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_STARTS=Verifying index(es) '%s' for base DN '%s' |
| | | INFO_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_CONSISTENT=Index(es) '%s' for base DN '%s' match the entries |
| | | INFO_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_INCONSISTENT=Index(es) '%s' for base DN '%s' do not match \ |
| | | the entries and are rebuilt |
| | | WARN_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_FAILED=Index(es) '%s' for base DN '%s' could not be verified, \ |
| | | see the upgrade log. They are left as they were, and the upgrade goes on: verify them manually with \ |
| | | verify-index once the backend can be read, and rebuild them with rebuild-index where it reports errors |
| | | WARN_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_SKIPPED=Index(es) '%s' for base DN '%s' were not verified, as \ |
| | | the rebuild of other indexes failed. Once the cause of that failure is fixed, verify them manually with \ |
| | | verify-index, and rebuild them with rebuild-index where it reports errors |
| | | INFO_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_DECLINED=You have to verify the equality indexes of the \ |
| | | attributes holding DNs manually with verify-index, and rebuild them with rebuild-index where it reports \ |
| | | errors, to get a fully functional server |
| 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.guitools.controlpanel.ui; |
| | | |
| | | import static org.assertj.core.api.Assertions.assertThat; |
| | | |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.RDN; |
| | | import org.opends.server.DirectoryServerTestCase; |
| | | import org.testng.annotations.DataProvider; |
| | | import org.testng.annotations.Test; |
| | | |
| | | /** Tests the DN that the new entry panels show for the naming value typed by the user (issue #1153). */ |
| | | @SuppressWarnings("javadoc") |
| | | public class NewEntryDNTestCase extends DirectoryServerTestCase |
| | | { |
| | | private static final String PARENT = "ou=People,dc=example,dc=com"; |
| | | |
| | | @DataProvider |
| | | public Object[][] namingValues() |
| | | { |
| | | return new Object[][] { { "John Smith" }, { "Smith, John" }, { "a+b" }, { "#1 fan" }, { "Smith;Jr" }, |
| | | { "a\\b" }, { "a=b" }, { " lead" } }; |
| | | } |
| | | |
| | | @Test(dataProvider = "namingValues") |
| | | public void newEntryDNUnderAParentNodeKeepsTheWholeValue(String value) |
| | | { |
| | | assertNewEntryDN(AbstractNewEntryPanel.getNewEntryDN("cn", value, DN.valueOf(PARENT)), value); |
| | | } |
| | | |
| | | @Test(dataProvider = "namingValues") |
| | | public void newEntryDNUnderATypedParentKeepsTheWholeValue(String value) |
| | | { |
| | | assertNewEntryDN(AbstractNewEntryPanel.getNewEntryDN("cn", value, PARENT), value); |
| | | } |
| | | |
| | | @Test |
| | | public void newEntryDNUnderAParentThatIsStillBeingTypedEscapesTheValue() |
| | | { |
| | | assertThat(AbstractNewEntryPanel.getNewEntryDN("cn", "Smith, John", "ou=People,dc")).isEqualTo( |
| | | "cn=Smith\\, John,ou=People,dc"); |
| | | } |
| | | |
| | | private static void assertNewEntryDN(String dnString, String value) |
| | | { |
| | | final DN dn = DN.valueOf(dnString); |
| | | assertThat((Object) dn.parent()).isEqualTo(DN.valueOf(PARENT)); |
| | | final RDN rdn = dn.rdn(); |
| | | assertThat(rdn.size()).isEqualTo(1); |
| | | assertThat(rdn.getFirstAVA().getAttributeType().hasName("cn")).isTrue(); |
| | | assertThat((Object) rdn.getFirstAVA().getAttributeValue()).isEqualTo(ByteString.valueOfUtf8(value)); |
| | | } |
| | | } |
| 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.guitools.controlpanel.util; |
| | | |
| | | import static org.assertj.core.api.Assertions.assertThat; |
| | | |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.RDN; |
| | | import org.opends.server.DirectoryServerTestCase; |
| | | import org.testng.annotations.DataProvider; |
| | | import org.testng.annotations.Test; |
| | | |
| | | /** |
| | | * Tests the configuration DNs that the control panel and the installer build from a backend ID and an index name |
| | | * (issue #1153). Neither has a pattern, so they may hold any character a DN has to escape. |
| | | */ |
| | | @SuppressWarnings("javadoc") |
| | | public class ConfigDNTestCase extends DirectoryServerTestCase |
| | | { |
| | | private static final DN BACKENDS = DN.valueOf("cn=Backends,cn=config"); |
| | | |
| | | @DataProvider |
| | | public Object[][] names() |
| | | { |
| | | return new Object[][] { |
| | | { "userRoot" }, |
| | | { "monitor,ou=a b" }, |
| | | { "a+sn=b" }, |
| | | { "a\\b" }, |
| | | { " a " }, |
| | | }; |
| | | } |
| | | |
| | | @Test(dataProvider = "names") |
| | | public void backendConfigDNNamesTheBackendBelowCnBackends(String backendID) |
| | | { |
| | | final DN dn = Utilities.getBackendConfigDN(backendID); |
| | | assertThat((Object) dn.parent()).isEqualTo(BACKENDS); |
| | | assertThat((Object) dn.rdn()).isEqualTo(new RDN("ds-cfg-backend-id", backendID)); |
| | | } |
| | | |
| | | @Test(dataProvider = "names") |
| | | public void indexConfigDNNamesTheIndexBelowItsBackend(String name) |
| | | { |
| | | final DN backendDN = BACKENDS.child("ds-cfg-backend-id", name); |
| | | |
| | | final DN index = Utilities.getIndexConfigDN(name, name, false); |
| | | assertThat((Object) index.parent(2)).isEqualTo(backendDN); |
| | | assertThat((Object) index.parent().rdn()).isEqualTo(new RDN("cn", "Index")); |
| | | assertThat((Object) index.rdn()).isEqualTo(new RDN("ds-cfg-attribute", name)); |
| | | |
| | | final DN vlvIndex = Utilities.getIndexConfigDN(name, name, true); |
| | | assertThat((Object) vlvIndex.parent(2)).isEqualTo(backendDN); |
| | | assertThat((Object) vlvIndex.parent().rdn()).isEqualTo(new RDN("cn", "VLV Index")); |
| | | assertThat((Object) vlvIndex.rdn()).isEqualTo(new RDN("ds-cfg-name", name)); |
| | | } |
| | | } |
| 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.guitools.controlpanel.util; |
| | | |
| | | import static org.assertj.core.api.Assertions.assertThat; |
| | | |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.opends.server.DirectoryServerTestCase; |
| | | import org.testng.annotations.DataProvider; |
| | | import org.testng.annotations.Test; |
| | | |
| | | /** Tests {@link Utilities#unescapeUtf8(String)}, which the control panel applies to DN strings (issue #1153). */ |
| | | @SuppressWarnings("javadoc") |
| | | public class UnescapeUtf8TestCase extends DirectoryServerTestCase |
| | | { |
| | | @DataProvider |
| | | public Object[][] dns() |
| | | { |
| | | return new Object[][] { |
| | | { "cn=a\\\\41,dc=x" }, |
| | | { "cn=a\\\\\\\\41,dc=x" }, |
| | | { "cn=J\\C3\\B6rg\\\\C3\\\\B6,dc=x" }, |
| | | { "cn=Before\\0dAfter,dc=x" }, |
| | | { "cn=Smith\\, John,dc=x" }, |
| | | }; |
| | | } |
| | | |
| | | /** The displayed DN must still be the same DN once the user selects it and the control panel parses it back. */ |
| | | @Test(dataProvider = "dns") |
| | | public void unescapedDNParsesBackToTheSameDN(String dnString) |
| | | { |
| | | final DN dn = DN.valueOf(dnString); |
| | | assertThat((Object) DN.valueOf(Utilities.unescapeUtf8(dn.toString()))).isEqualTo(dn); |
| | | } |
| | | |
| | | @Test |
| | | public void escapedBackslashIsNotTheStartOfAHexPair() |
| | | { |
| | | assertThat(Utilities.unescapeUtf8("cn=a\\\\41,dc=x")).isEqualTo("cn=a\\\\41,dc=x"); |
| | | } |
| | | |
| | | @Test |
| | | public void hexPairsAreDecodedAsUtf8() |
| | | { |
| | | assertThat(Utilities.unescapeUtf8("cn=J\\C3\\B6rg,dc=x")).isEqualTo("cn=Jörg,dc=x"); |
| | | } |
| | | } |
| 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.quicksetup.installer; |
| | | |
| | | import static org.assertj.core.api.Assertions.assertThat; |
| | | |
| | | import java.util.Arrays; |
| | | |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.opends.server.DirectoryServerTestCase; |
| | | import org.opends.server.TestCaseUtils; |
| | | import org.opends.server.core.DirectoryServer; |
| | | import org.opends.server.types.Entry; |
| | | import org.testng.annotations.BeforeClass; |
| | | import org.testng.annotations.Test; |
| | | |
| | | /** |
| | | * Tests that the installer deletes the configuration of the backend it names, whatever its ID holds (issue #1153). |
| | | */ |
| | | @SuppressWarnings("javadoc") |
| | | public class DeleteBackendTestCase extends DirectoryServerTestCase |
| | | { |
| | | @BeforeClass |
| | | public void startServer() throws Exception |
| | | { |
| | | TestCaseUtils.startServer(); |
| | | } |
| | | |
| | | /** Concatenated into a DN, the ID "a,b" would name "ds-cfg-backend-id=a,b,cn=Backends,cn=config". */ |
| | | @Test |
| | | public void deletesTheBackendWhoseIDADNHasToEscape() throws Exception |
| | | { |
| | | final Entry backend = TestCaseUtils.makeEntry( |
| | | "dn: ds-cfg-backend-id=a\\,b,cn=Backends,cn=config", |
| | | "objectClass: top", |
| | | "objectClass: ds-cfg-backend", |
| | | "objectClass: ds-cfg-local-backend", |
| | | "objectClass: ds-cfg-pluggable-backend", |
| | | "objectClass: ds-cfg-pdb-backend", |
| | | "ds-cfg-enabled: false", |
| | | "ds-cfg-java-class: org.opends.server.backends.pdb.PDBBackend", |
| | | "ds-cfg-backend-id: a,b", |
| | | "ds-cfg-writability-mode: enabled", |
| | | "ds-cfg-base-dn: o=delete backend test", |
| | | "ds-cfg-db-directory: db_delete_backend_test"); |
| | | final Entry indexBranch = TestCaseUtils.makeEntry( |
| | | "dn: cn=Index,ds-cfg-backend-id=a\\,b,cn=Backends,cn=config", |
| | | "objectClass: top", |
| | | "objectClass: ds-cfg-branch", |
| | | "cn: Index"); |
| | | try |
| | | { |
| | | TestCaseUtils.addEntry(backend); |
| | | TestCaseUtils.addEntry(indexBranch); |
| | | |
| | | new InstallerHelper().deleteBackend("a,b"); |
| | | |
| | | assertThat(DirectoryServer.entryExists(indexBranch.getName())).isFalse(); |
| | | assertThat(DirectoryServer.entryExists(backend.getName())).isFalse(); |
| | | } |
| | | finally |
| | | { |
| | | for (final DN dn : Arrays.asList(indexBranch.getName(), backend.getName())) |
| | | { |
| | | if (DirectoryServer.entryExists(dn)) |
| | | { |
| | | TestCaseUtils.deleteEntry(dn); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| 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.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(); |
| | | } |
| | | } |
| 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.authorization.dseecompat; |
| | | |
| | | import static org.assertj.core.api.Assertions.*; |
| | | |
| | | import java.lang.reflect.Field; |
| | | |
| | | 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 UserAttr#decode(String, EnumBindRuleType)} splits a userattr expression at its first |
| | | * octothorpe: the attribute name cannot contain one, the value after it can (issue #1153). |
| | | */ |
| | | @SuppressWarnings("javadoc") |
| | | public class UserAttrTest extends DirectoryServerTestCase |
| | | { |
| | | @BeforeClass |
| | | public void setUp() throws Exception |
| | | { |
| | | TestCaseUtils.startFakeServer(); |
| | | } |
| | | |
| | | @AfterClass |
| | | public void tearDown() throws DirectoryException |
| | | { |
| | | TestCaseUtils.shutdownFakeServer(); |
| | | } |
| | | |
| | | @DataProvider |
| | | public Object[][] valueExpressions() |
| | | { |
| | | return new Object[][] { |
| | | { "departmentNumber#a1", "departmentNumber", "a1" }, |
| | | { "departmentNumber#a#1", "departmentNumber", "a#1" }, |
| | | { "departmentNumber##", "departmentNumber", "#" }, |
| | | }; |
| | | } |
| | | |
| | | @Test(dataProvider = "valueExpressions") |
| | | public void decodeSplitsAtTheFirstOctothorpe(String expression, String attrName, String value) throws Exception |
| | | { |
| | | final KeywordBindRule rule = UserAttr.decode(expression, EnumBindRuleType.EQUAL_BINDRULE_TYPE); |
| | | |
| | | assertThat(field(rule, "attrStr")).isEqualTo(attrName); |
| | | assertThat(field(rule, "attrVal")).isEqualTo(value); |
| | | } |
| | | |
| | | @DataProvider |
| | | public Object[][] invalidExpressions() |
| | | { |
| | | return new Object[][] { { "departmentNumber" }, { "departmentNumber#" } }; |
| | | } |
| | | |
| | | @Test(dataProvider = "invalidExpressions", expectedExceptions = AciException.class) |
| | | public void decodeRejectsAnExpressionWithoutValue(String expression) throws Exception |
| | | { |
| | | UserAttr.decode(expression, EnumBindRuleType.EQUAL_BINDRULE_TYPE); |
| | | } |
| | | |
| | | private static Object field(Object object, String name) throws Exception |
| | | { |
| | | final Field field = UserAttr.class.getDeclaredField(name); |
| | | field.setAccessible(true); |
| | | return field.get(object); |
| | | } |
| | | } |
| 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.pluggable; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.schema.AttributeType; |
| | | import org.opends.server.api.LocalBackend; |
| | | import org.opends.server.backends.pluggable.spi.Cursor; |
| | | import org.opends.server.core.DirectoryServer; |
| | | |
| | | /** |
| | | * Removes the keys of an attribute's indexes while leaving them trusted, as an index whose keys a previous version |
| | | * computed differently looks to the current one. For the tests outside this package, which cannot reach the trees |
| | | * of a backend. |
| | | */ |
| | | public final class IndexKeyRemover |
| | | { |
| | | private IndexKeyRemover() |
| | | { |
| | | // Utility class |
| | | } |
| | | |
| | | /** |
| | | * Removes every key of the indexes of an attribute under a base DN of an online pluggable backend, and marks |
| | | * these indexes trusted. |
| | | * |
| | | * @param backend |
| | | * The backend, which must be a pluggable one. |
| | | * @param baseDN |
| | | * The base DN of the entry container. |
| | | * @param attributeName |
| | | * The name of the indexed attribute. |
| | | * @throws Exception |
| | | * If the keys cannot be removed. |
| | | */ |
| | | public static void removeAllKeys(LocalBackend<?> backend, DN baseDN, String attributeName) throws Exception |
| | | { |
| | | final RootContainer rootContainer = ((BackendImpl<?>) backend).getRootContainer(); |
| | | final AttributeType attributeType = |
| | | DirectoryServer.getInstance().getServerContext().getSchema().getAttributeType(attributeName); |
| | | final AttributeIndex attributeIndex = rootContainer.getEntryContainer(baseDN).getAttributeIndex(attributeType); |
| | | rootContainer.getStorage().write(txn -> |
| | | { |
| | | for (AttributeIndex.MatchingRuleIndex index : attributeIndex.getNameToIndexes().values()) |
| | | { |
| | | final List<ByteString> keys = new ArrayList<>(); |
| | | try (Cursor<ByteString, ByteString> cursor = txn.openCursor(index.getName())) |
| | | { |
| | | while (cursor.next()) |
| | | { |
| | | keys.add(cursor.getKey()); |
| | | } |
| | | } |
| | | for (ByteString key : keys) |
| | | { |
| | | txn.delete(index.getName(), key); |
| | | } |
| | | // A missing key of an untrusted index is not an error, as the index is known to be incomplete |
| | | index.setTrusted(txn, true); |
| | | } |
| | | }); |
| | | } |
| | | } |
| 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.config; |
| | | |
| | | import static org.assertj.core.api.Assertions.*; |
| | | |
| | | import java.util.HashSet; |
| | | import java.util.Set; |
| | | |
| | | import javax.management.ObjectName; |
| | | |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.opends.server.DirectoryServerTestCase; |
| | | import org.testng.annotations.DataProvider; |
| | | import org.testng.annotations.Test; |
| | | |
| | | /** Tests the JMX object names that {@link JMXMBean#getJmxName(DN)} builds from DNs (issue #1153). */ |
| | | @SuppressWarnings("javadoc") |
| | | public class JMXMBeanNameTest extends DirectoryServerTestCase |
| | | { |
| | | /** Names whose values hold only letters, digits and spaces keep the name they always had. */ |
| | | @DataProvider |
| | | public Object[][] unchangedNames() |
| | | { |
| | | return new Object[][] { |
| | | { "cn=monitor", "org.opends.server:Name=rootDSE,Rdn1=cn-monitor" }, |
| | | { "ds-cfg-backend-id=userRoot,cn=Backends,cn=config", |
| | | "org.opends.server:Name=rootDSE,Rdn1=cn-config,Rdn2=cn-Backends,Rdn3=dscfgbackendid-userRoot" }, |
| | | { "cn=JVM Memory Usage,cn=monitor", "org.opends.server:Name=rootDSE,Rdn1=cn-monitor,Rdn2=cn-JVM_Memory_Usage" }, |
| | | }; |
| | | } |
| | | |
| | | @Test(dataProvider = "unchangedNames") |
| | | public void plainNamesAreUnchanged(String dn, String expectedName) throws Exception |
| | | { |
| | | assertThat(JMXMBean.getJmxName(DN.valueOf(dn))).isEqualTo(expectedName); |
| | | } |
| | | |
| | | /** |
| | | * The SNMP extension finds the connection handlers and their statistics by "Connection_Handler" and |
| | | * "_Statistics" in these names, so a space stays '_' in a percent-encoded value too. |
| | | */ |
| | | @DataProvider |
| | | public Object[][] connectionHandlerNames() |
| | | { |
| | | return new Object[][] { |
| | | { "cn=LDAP Connection Handler 0.0.0.0 port 1389,cn=monitor", |
| | | "org.opends.server:Name=rootDSE,Rdn1=cn-monitor,Rdn2=cn-LDAP_Connection_Handler_0%2E0%2E0%2E0_port_1389" }, |
| | | { "cn=LDAP Connection Handler 0.0.0.0 port 1389 Statistics,cn=monitor", |
| | | "org.opends.server:Name=rootDSE,Rdn1=cn-monitor," |
| | | + "Rdn2=cn-LDAP_Connection_Handler_0%2E0%2E0%2E0_port_1389_Statistics" }, |
| | | }; |
| | | } |
| | | |
| | | @Test(dataProvider = "connectionHandlerNames") |
| | | public void encodedValuesKeepUnderscoreForSpace(String dn, String expectedName) throws Exception |
| | | { |
| | | assertThat(JMXMBean.getJmxName(DN.valueOf(dn))).isEqualTo(expectedName); |
| | | } |
| | | |
| | | /** Each group holds DNs whose values only differ by characters that the old mapping dropped. */ |
| | | @DataProvider |
| | | public Object[][] distinctDNs() |
| | | { |
| | | return new Object[][] { |
| | | { new String[] { "ds-cfg-backend-id=user-root,cn=Backends,cn=config", |
| | | "ds-cfg-backend-id=userroot,cn=Backends,cn=config", |
| | | "ds-cfg-backend-id=user_root,cn=Backends,cn=config", |
| | | "ds-cfg-backend-id=user root,cn=Backends,cn=config", |
| | | "ds-cfg-backend-id=user%20root,cn=Backends,cn=config" } }, |
| | | { new String[] { "cn=a\\,b,cn=monitor", "cn=ab,cn=monitor", "cn=a,cn=b,cn=monitor", "cn=a+sn=b,cn=monitor", |
| | | "cn=a\\+sn\\=b,cn=monitor", "cn=asn\\=b,cn=monitor" } }, |
| | | { new String[] { "cn=J\\C3\\B6rg,cn=monitor", "cn=Jrg,cn=monitor", "cn=J\\C3\\A4rg,cn=monitor" } }, |
| | | { new String[] { "cn=a.b c,cn=monitor", "cn=a.b_c,cn=monitor", "cn=a.b%20c,cn=monitor", |
| | | "cn=a.b%5Fc,cn=monitor" } }, |
| | | }; |
| | | } |
| | | |
| | | @Test(dataProvider = "distinctDNs") |
| | | public void distinctDNsGetDistinctValidNames(String[] dns) throws Exception |
| | | { |
| | | final Set<String> names = new HashSet<>(); |
| | | for (String dn : dns) |
| | | { |
| | | final String name = JMXMBean.getJmxName(DN.valueOf(dn)); |
| | | assertThat(name).as("JMX name of %s", dn).isNotNull(); |
| | | assertThat(new ObjectName(name).isPattern()).as("JMX name %s", name).isFalse(); |
| | | names.add(name); |
| | | } |
| | | assertThat(names).hasSize(dns.length); |
| | | } |
| | | } |
| 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.monitors; |
| | | |
| | | import static org.assertj.core.api.Assertions.*; |
| | | import static org.forgerock.opendj.ldap.SearchScope.*; |
| | | import static org.opends.server.protocols.internal.InternalClientConnection.*; |
| | | import static org.opends.server.protocols.internal.Requests.*; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.Arrays; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.opendj.server.config.server.MonitorProviderCfg; |
| | | import org.opends.server.TestCaseUtils; |
| | | import org.opends.server.api.ConnectionHandler; |
| | | import org.opends.server.api.MonitorData; |
| | | import org.opends.server.api.MonitorProvider; |
| | | import org.opends.server.core.DirectoryServer; |
| | | import org.opends.server.protocols.http.HTTPConnectionHandler; |
| | | import org.opends.server.protocols.internal.InternalSearchOperation; |
| | | import org.opends.server.protocols.ldap.LDAPConnectionHandler; |
| | | import org.opends.server.types.Entry; |
| | | import org.forgerock.opendj.reactive.LDAPConnectionHandler2; |
| | | import org.testng.annotations.BeforeClass; |
| | | import org.testng.annotations.DataProvider; |
| | | import org.testng.annotations.Test; |
| | | |
| | | /** |
| | | * A monitor provider whose name holds ',', '+' or '\' must get the monitor entry it names, and must not |
| | | * break the rest of cn=monitor (issue #1153). |
| | | */ |
| | | @SuppressWarnings("javadoc") |
| | | public class MonitorDNTestCase extends MonitorTestCase |
| | | { |
| | | private static final DN MONITOR_ROOT = DN.valueOf("cn=monitor"); |
| | | |
| | | /** A monitor provider of a third party, which gives its name as is. */ |
| | | private static final class NamedMonitorProvider extends MonitorProvider<MonitorProviderCfg> |
| | | { |
| | | private final String name; |
| | | |
| | | NamedMonitorProvider(String name) |
| | | { |
| | | this.name = name; |
| | | } |
| | | |
| | | @Override |
| | | public void initializeMonitorProvider(MonitorProviderCfg configuration) |
| | | { |
| | | // No implementation required. |
| | | } |
| | | |
| | | @Override |
| | | public String getMonitorInstanceName() |
| | | { |
| | | return name; |
| | | } |
| | | |
| | | @Override |
| | | public MonitorData getMonitorData() |
| | | { |
| | | return new MonitorData(0); |
| | | } |
| | | } |
| | | |
| | | @BeforeClass |
| | | public void startServer() throws Exception |
| | | { |
| | | TestCaseUtils.startServer(); |
| | | } |
| | | |
| | | /** The name of {@link TestMonitorProvider} is a relative DN: it still names an entry two levels down. */ |
| | | @Test |
| | | public void relativeDNNameKeepsItsTree() |
| | | { |
| | | assertThat((Object) DirectoryServer.getMonitorProviderDN(new TestMonitorProvider())) |
| | | .isEqualTo(DN.valueOf("cn=Test monitor for dc=example,dc=com,cn=monitor")); |
| | | } |
| | | |
| | | @DataProvider |
| | | public Object[][] namesThatAreNotRelativeDNs() |
| | | { |
| | | return new Object[][] { { "LDAP, internal 0.0.0.0 port 41390" }, { "a+b" }, { "a;b" }, { "x\\" } }; |
| | | } |
| | | |
| | | @Test(dataProvider = "namesThatAreNotRelativeDNs") |
| | | public void nameThatIsNotARelativeDNNamesOneEntry(String name) throws Exception |
| | | { |
| | | final NamedMonitorProvider provider = new NamedMonitorProvider(name); |
| | | assertThat((Object) DirectoryServer.getMonitorProviderDN(provider)).isEqualTo(MONITOR_ROOT.child("cn", name)); |
| | | |
| | | DirectoryServer.registerMonitorProvider(provider); |
| | | try |
| | | { |
| | | assertMonitorIsSearchable(); |
| | | assertEntryExists(MONITOR_ROOT.child("cn", name)); |
| | | } |
| | | finally |
| | | { |
| | | DirectoryServer.deregisterMonitorProvider(provider); |
| | | } |
| | | } |
| | | |
| | | @DataProvider |
| | | public Object[][] connectionHandlerClasses() |
| | | { |
| | | final String[] ldap = { "ds-cfg-allow-ldap-v2: false", "ds-cfg-allow-start-tls: false" }; |
| | | return new Object[][] { |
| | | { LDAPConnectionHandler.class.getName(), "ds-cfg-ldap-connection-handler", ldap }, |
| | | { LDAPConnectionHandler2.class.getName(), "ds-cfg-ldap-connection-handler", ldap }, |
| | | { HTTPConnectionHandler.class.getName(), "ds-cfg-http-connection-handler", new String[0] }, |
| | | }; |
| | | } |
| | | |
| | | /** The connection handler, its client connections and its statistics each get the entry they name. */ |
| | | @Test(dataProvider = "connectionHandlerClasses") |
| | | public void connectionHandlerWithACommaInItsName(String javaClass, String objectClass, String[] attributes) |
| | | throws Exception |
| | | { |
| | | // Unescaped, "LDAP,ou=internal 127.0.0.1 port N" would be a relative DN two levels down |
| | | final String name = "LDAP,ou=internal"; |
| | | final int port = TestCaseUtils.findFreePort(); |
| | | final List<String> ldif = new ArrayList<>(Arrays.asList( |
| | | "dn: cn=LDAP\\,ou\\=internal,cn=Connection Handlers,cn=config", |
| | | "objectClass: top", |
| | | "objectClass: ds-cfg-connection-handler", |
| | | "objectClass: " + objectClass, |
| | | "cn: " + name, |
| | | "ds-cfg-java-class: " + javaClass, |
| | | "ds-cfg-enabled: true", |
| | | "ds-cfg-listen-address: 127.0.0.1", |
| | | "ds-cfg-listen-port: " + port, |
| | | "ds-cfg-use-ssl: false")); |
| | | Collections.addAll(ldif, attributes); |
| | | final Entry handlerEntry = TestCaseUtils.makeEntry(ldif.toArray(new String[0])); |
| | | TestCaseUtils.addEntry(handlerEntry); |
| | | try |
| | | { |
| | | final String handlerName = connectionHandlerName(name); |
| | | assertMonitorIsSearchable(); |
| | | assertEntryExists(MONITOR_ROOT.child("cn", handlerName)); |
| | | assertEntryExists(MONITOR_ROOT.child("cn", handlerName).child("cn", "Client Connections")); |
| | | assertEntryExists(MONITOR_ROOT.child("cn", handlerName + " Statistics")); |
| | | } |
| | | finally |
| | | { |
| | | TestCaseUtils.deleteEntry(handlerEntry); |
| | | } |
| | | } |
| | | |
| | | @DataProvider |
| | | public Object[][] storageBackends() |
| | | { |
| | | return new Object[][] { |
| | | { "ds-cfg-je-backend", "org.opends.server.backends.jeb.JEBackend", " JE Database" }, |
| | | { "ds-cfg-pdb-backend", "org.opends.server.backends.pdb.PDBBackend", " PDB Database" }, |
| | | }; |
| | | } |
| | | |
| | | /** A backend whose ID holds ',' and '=' gets its backend, storage, database and disk space monitor entries. */ |
| | | @Test(dataProvider = "storageBackends") |
| | | public void backendWithACommaInItsID(String objectClass, String javaClass, String databaseSuffix) throws Exception |
| | | { |
| | | // Unescaped, "monitor,ou=a b Backend" would be a relative DN two levels down |
| | | final String backendID = "monitor,ou=a b"; |
| | | final Entry backendEntry = TestCaseUtils.makeEntry( |
| | | "dn: ds-cfg-backend-id=monitor\\,ou\\=a b,cn=Backends,cn=config", |
| | | "objectClass: top", |
| | | "objectClass: ds-cfg-backend", |
| | | "objectClass: ds-cfg-local-backend", |
| | | "objectClass: ds-cfg-pluggable-backend", |
| | | "objectClass: " + objectClass, |
| | | "ds-cfg-enabled: true", |
| | | "ds-cfg-java-class: " + javaClass, |
| | | "ds-cfg-backend-id: " + backendID, |
| | | "ds-cfg-writability-mode: enabled", |
| | | "ds-cfg-base-dn: o=monitor dn test", |
| | | "ds-cfg-db-directory: db_monitor_dn_test", |
| | | "ds-cfg-db-cache-percent: 2"); |
| | | TestCaseUtils.addEntry(backendEntry); |
| | | try |
| | | { |
| | | assertMonitorIsSearchable(); |
| | | assertEntryExists(MONITOR_ROOT.child("cn", backendID + " Backend")); |
| | | assertEntryExists(MONITOR_ROOT.child("cn", backendID + " Storage")); |
| | | assertEntryExists(MONITOR_ROOT.child("cn", backendID + databaseSuffix)); |
| | | assertEntryExists(MONITOR_ROOT.child("cn", "Disk Space Monitor").child("cn", backendID + " backend")); |
| | | } |
| | | finally |
| | | { |
| | | TestCaseUtils.deleteEntry(backendEntry); |
| | | } |
| | | } |
| | | |
| | | /** An entry cache whose name holds ',' and '=' gets its monitor entry. */ |
| | | @Test |
| | | public void entryCacheWithACommaInItsName() throws Exception |
| | | { |
| | | // Unescaped, "FIFO,ou=a b Entry Cache" would be a relative DN two levels down |
| | | final String cacheName = "FIFO,ou=a b"; |
| | | final Entry cacheEntry = TestCaseUtils.makeEntry( |
| | | "dn: cn=FIFO\\,ou\\=a b,cn=Entry Caches,cn=config", |
| | | "objectClass: top", |
| | | "objectClass: ds-cfg-entry-cache", |
| | | "objectClass: ds-cfg-fifo-entry-cache", |
| | | "cn: " + cacheName, |
| | | "ds-cfg-cache-level: 7", |
| | | "ds-cfg-java-class: org.opends.server.extensions.FIFOEntryCache", |
| | | "ds-cfg-enabled: true", |
| | | "ds-cfg-max-entries: 10"); |
| | | TestCaseUtils.addEntry(cacheEntry); |
| | | try |
| | | { |
| | | assertMonitorIsSearchable(); |
| | | assertEntryExists(MONITOR_ROOT.child("cn", cacheName + " Entry Cache")); |
| | | } |
| | | finally |
| | | { |
| | | TestCaseUtils.deleteEntry(cacheEntry); |
| | | } |
| | | } |
| | | |
| | | /** The name of a connection handler starts with the name of its configuration entry, then its address and port. */ |
| | | private static String connectionHandlerName(String configName) |
| | | { |
| | | for (ConnectionHandler<?> handler : DirectoryServer.getConnectionHandlers()) |
| | | { |
| | | if (handler.getConnectionHandlerName().startsWith(configName + " ")) |
| | | { |
| | | return handler.getConnectionHandlerName(); |
| | | } |
| | | } |
| | | return fail("no connection handler named " + configName); |
| | | } |
| | | |
| | | private static void assertMonitorIsSearchable() throws Exception |
| | | { |
| | | final InternalSearchOperation subtree = getRootConnection().processSearch(newSearchRequest(MONITOR_ROOT, WHOLE_SUBTREE)); |
| | | assertThat(subtree.getResultCode()).as(String.valueOf(subtree.getErrorMessage())).isEqualTo(ResultCode.SUCCESS); |
| | | assertEntryExists(DN.valueOf("cn=Version,cn=monitor")); |
| | | } |
| | | |
| | | /** Asserts that a monitor provider gives the entry: a branch (glue) entry would not do. */ |
| | | private static void assertEntryExists(DN dn) throws Exception |
| | | { |
| | | final InternalSearchOperation base = getRootConnection().processSearch( |
| | | newSearchRequest(dn, BASE_OBJECT, "(!(objectClass=ds-mon-branch))")); |
| | | assertThat(base.getResultCode()).as(dn + ": " + base.getErrorMessage()).isEqualTo(ResultCode.SUCCESS); |
| | | assertThat(base.getSearchEntries()).hasSize(1); |
| | | } |
| | | } |
| | |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2013-2016 ForgeRock AS. |
| | | * Portions Copyright 2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.replication.common; |
| | | |
| | |
| | | } |
| | | |
| | | @DataProvider |
| | | public Object[][] specialBaseDNs() |
| | | { |
| | | return new Object[][] { |
| | | { "o=urn:x" }, |
| | | { "ou=https://idp.example.com/metadata#v1,dc=x" }, |
| | | // DN.toString() escapes ';' as "\;" |
| | | { "o=a\\;b" }, |
| | | // An escaped backslash does not escape the ';' that follows it |
| | | { "o=a\\\\\\;b" }, |
| | | { "o=a\\\\" }, |
| | | { "o=a\\:b" }, |
| | | }; |
| | | } |
| | | |
| | | /** The cookie written by toString() must decode to the same state whatever the base DN contains. */ |
| | | @Test(dataProvider = "specialBaseDNs") |
| | | public void decodeCookieWithSpecialBaseDN(String baseDN) throws Exception |
| | | { |
| | | final DN dn = DN.valueOf(baseDN); |
| | | final MultiDomainServerState state = new MultiDomainServerState(); |
| | | state.update(dn, csn1); |
| | | state.update(dn2, csn2); |
| | | final String cookie = state.toString(); |
| | | |
| | | final MultiDomainServerState decoded = new MultiDomainServerState(cookie); |
| | | assertEquals(decoded.getCSN(dn, csn1.getServerId()), csn1); |
| | | assertEquals(decoded.getCSN(dn2, csn2.getServerId()), csn2); |
| | | assertEquals(decoded.toString(), cookie); |
| | | } |
| | | |
| | | @DataProvider |
| | | public Object[][] copyCtorData() |
| | | { |
| | | return new Object[][] { |
| 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.replication.server; |
| | | |
| | | import static org.assertj.core.api.Assertions.*; |
| | | |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.opends.server.DirectoryServerTestCase; |
| | | import org.testng.annotations.DataProvider; |
| | | import org.testng.annotations.Test; |
| | | |
| | | /** Tests the monitor entry that the name of a {@link ReplicationServerDomain} gives (issue #1153). */ |
| | | @SuppressWarnings("javadoc") |
| | | public class ReplicationServerDomainMonitorNameTest extends DirectoryServerTestCase |
| | | { |
| | | @DataProvider |
| | | public Object[][] baseDNs() |
| | | { |
| | | return new Object[][] { |
| | | { "dc=example,dc=com", "dc_example_dc_com" }, |
| | | // An RDN with two AVAs: the '+' must not start a second AVA in the monitor entry RDN |
| | | { "cn=a+sn=b,dc=x", "cn_a+sn_b_dc_x" }, |
| | | { "o=a\\,b", "o_a\\_b" }, |
| | | }; |
| | | } |
| | | |
| | | @Test(dataProvider = "baseDNs") |
| | | public void monitorNameIsTheRelativeDNOfTheDomainMonitorEntry(String baseDN, String domainName) |
| | | { |
| | | final String name = ReplicationServerDomain.getMonitorInstanceName(2, "host:8989", DN.valueOf(baseDN)); |
| | | |
| | | assertThat((Object) DN.valueOf("cn=" + name + ",cn=monitor")).isEqualTo(DN.valueOf("cn=monitor") |
| | | .child("cn", "Replication").child("cn", domainName).child("cn", "Replication server RS(2) host:8989")); |
| | | } |
| | | } |
| 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.replication.service; |
| | | |
| | | import static org.assertj.core.api.Assertions.*; |
| | | |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.opends.server.DirectoryServerTestCase; |
| | | import org.opends.server.types.HostPort; |
| | | import org.testng.annotations.DataProvider; |
| | | import org.testng.annotations.Test; |
| | | |
| | | /** Tests the monitor entry that the name of a {@link ReplicationMonitor} gives (issue #1153). */ |
| | | @SuppressWarnings("javadoc") |
| | | public class ReplicationMonitorNameTest extends DirectoryServerTestCase |
| | | { |
| | | @DataProvider |
| | | public Object[][] baseDNs() |
| | | { |
| | | return new Object[][] { |
| | | { "dc=example,dc=com", "dc_example_dc_com" }, |
| | | // An RDN with two AVAs: the '+' must not start a second AVA in the monitor entry RDN |
| | | { "cn=a+sn=b,dc=x", "cn_a+sn_b_dc_x" }, |
| | | { "o=a\\,b", "o_a\\_b" }, |
| | | }; |
| | | } |
| | | |
| | | @Test(dataProvider = "baseDNs") |
| | | public void monitorNameIsTheRelativeDNOfTheDomainMonitorEntry(String baseDN, String domainName) |
| | | { |
| | | final String name = ReplicationMonitor.getMonitorInstanceName(1, new HostPort("host", 1389), DN.valueOf(baseDN)); |
| | | |
| | | assertThat((Object) DN.valueOf("cn=" + name + ",cn=monitor")).isEqualTo(DN.valueOf("cn=monitor") |
| | | .child("cn", "Replication").child("cn", domainName).child("cn", "Directory server DS(1) host:1389")); |
| | | } |
| | | } |
| 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.snmp; |
| | | |
| | | import static org.assertj.core.api.Assertions.assertThat; |
| | | |
| | | import java.lang.reflect.Constructor; |
| | | |
| | | import javax.management.MBeanServer; |
| | | import javax.management.MBeanServerFactory; |
| | | import javax.management.ObjectName; |
| | | import javax.management.modelmbean.RequiredModelMBean; |
| | | |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.opends.server.DirectoryServerTestCase; |
| | | import org.opends.server.TestCaseUtils; |
| | | import org.opends.server.config.JMXMBean; |
| | | import org.testng.annotations.BeforeClass; |
| | | import org.testng.annotations.DataProvider; |
| | | import org.testng.annotations.Test; |
| | | |
| | | /** |
| | | * Verifies that the SNMP extension still finds a connection handler and its statistics by the JMX names that |
| | | * {@link JMXMBean#getJmxName(DN)} gives their monitor entries, including a handler whose name holds an IP address |
| | | * and is therefore percent-encoded (issue #1153). |
| | | */ |
| | | @SuppressWarnings("javadoc") |
| | | public class SNMPMonitorConnectionHandlerNameTest extends DirectoryServerTestCase |
| | | { |
| | | /** {@link SNMPMonitor} acts through the root internal connection, which needs a started server. */ |
| | | @BeforeClass |
| | | public void setUp() throws Exception |
| | | { |
| | | TestCaseUtils.startServer(); |
| | | } |
| | | |
| | | @DataProvider |
| | | public Object[][] handlerNames() |
| | | { |
| | | return new Object[][] { |
| | | { "LDAP Connection Handler 0.0.0.0 port 1389" }, |
| | | { "LDAPS Connection Handler 192.168.0.1 port 1636" }, |
| | | { "LDAP Connection Handler 0:0:0:0:0:0:0:0 port 1389" }, |
| | | { "LDAP Connection Handler localhost port 1389" }, |
| | | }; |
| | | } |
| | | |
| | | @Test(dataProvider = "handlerNames") |
| | | public void connectionHandlerAndItsStatisticsAreFound(String handlerName) throws Exception |
| | | { |
| | | // A server of its own, which MBeanServerFactory does not keep a reference to. |
| | | final MBeanServer server = MBeanServerFactory.newMBeanServer(); |
| | | final ObjectName handler = register(server, handlerName); |
| | | final ObjectName statistics = register(server, handlerName + " Statistics"); |
| | | final SNMPMonitor monitor = newMonitor(server); |
| | | |
| | | assertThat(monitor.getConnectionHandlers()).containsExactly(handler); |
| | | assertThat(monitor.getConnectionHandlersStatistics()).containsExactly(statistics); |
| | | assertThat(monitor.getConnectionHandlerStatistics(handler)).isEqualTo(statistics); |
| | | assertThat(monitor.getConnectionHandler(statistics)).isEqualTo(handler); |
| | | } |
| | | |
| | | private static ObjectName register(MBeanServer server, String monitorName) throws Exception |
| | | { |
| | | final ObjectName name = |
| | | new ObjectName(JMXMBean.getJmxName(DN.valueOf("cn=monitor").child("cn", monitorName))); |
| | | server.registerMBean(new RequiredModelMBean(), name); |
| | | return name; |
| | | } |
| | | |
| | | /** {@link SNMPMonitor#getMonitor(MBeanServer)} keeps the first server it is given, so build one per server. */ |
| | | private static SNMPMonitor newMonitor(MBeanServer server) throws Exception |
| | | { |
| | | final Constructor<SNMPMonitor> constructor = SNMPMonitor.class.getDeclaredConstructor(MBeanServer.class); |
| | | constructor.setAccessible(true); |
| | | return constructor.newInstance(server); |
| | | } |
| | | } |
| 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.tools.tasks; |
| | | |
| | | import static org.assertj.core.api.Assertions.*; |
| | | import static org.opends.server.config.ConfigConstants.*; |
| | | |
| | | import java.util.Arrays; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.RDN; |
| | | import org.opends.server.DirectoryServerTestCase; |
| | | import org.opends.server.types.RawAttribute; |
| | | import org.testng.annotations.DataProvider; |
| | | import org.testng.annotations.Test; |
| | | |
| | | /** Tests the task entry DN that {@link TaskClient#getTaskDN(List)} builds from the task ID (issue #1153). */ |
| | | @SuppressWarnings("javadoc") |
| | | public class TaskClientTaskDNTest extends DirectoryServerTestCase |
| | | { |
| | | @DataProvider |
| | | public Object[][] taskIDs() |
| | | { |
| | | return new Object[][] { { "daily" }, { "daily,full" }, { "a+b" }, { "#1" }, { "a\\b" }, { " lead" } }; |
| | | } |
| | | |
| | | @Test(dataProvider = "taskIDs") |
| | | public void recurringTaskDNKeepsTheWholeID(String taskID) |
| | | { |
| | | final List<RawAttribute> attributes = Arrays.asList( |
| | | RawAttribute.create(ATTR_TASK_ID, taskID), RawAttribute.create(ATTR_RECURRING_TASK_ID, taskID)); |
| | | |
| | | final DN taskDN = DN.valueOf(TaskClient.getTaskDN(attributes)); |
| | | |
| | | assertThat((Object) taskDN.parent()).isEqualTo(DN.valueOf(RECURRING_TASK_BASE_RDN + "," + DN_TASK_ROOT)); |
| | | assertSingleValuedRDN(taskDN.rdn(), ATTR_RECURRING_TASK_ID, taskID); |
| | | } |
| | | |
| | | @Test(dataProvider = "taskIDs") |
| | | public void scheduledTaskDNKeepsTheWholeID(String taskID) |
| | | { |
| | | final List<RawAttribute> attributes = Collections.singletonList(RawAttribute.create(ATTR_TASK_ID, taskID)); |
| | | |
| | | final DN taskDN = DN.valueOf(TaskClient.getTaskDN(attributes)); |
| | | |
| | | assertThat((Object) taskDN.parent()).isEqualTo(DN.valueOf(SCHEDULED_TASK_BASE_RDN + "," + DN_TASK_ROOT)); |
| | | assertSingleValuedRDN(taskDN.rdn(), ATTR_TASK_ID, taskID); |
| | | } |
| | | |
| | | private static void assertSingleValuedRDN(RDN rdn, String attributeName, String value) |
| | | { |
| | | assertThat(rdn.size()).isEqualTo(1); |
| | | assertThat(rdn.getFirstAVA().getAttributeType().hasName(attributeName)).isTrue(); |
| | | assertThat((Object) rdn.getFirstAVA().getAttributeValue()).isEqualTo(ByteString.valueOfUtf8(value)); |
| | | } |
| | | } |
| 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.tools.upgrade; |
| | | |
| | | import static java.nio.charset.StandardCharsets.UTF_8; |
| | | import static org.assertj.core.api.Assertions.assertThat; |
| | | import static org.assertj.core.api.Assertions.catchThrowable; |
| | | import static org.opends.messages.ToolMessages.INFO_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_CONSISTENT; |
| | | import static org.opends.messages.ToolMessages.INFO_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_INCONSISTENT; |
| | | import static org.opends.messages.ToolMessages.INFO_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_STARTS; |
| | | import static org.opends.messages.ToolMessages.WARN_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_FAILED; |
| | | import static org.opends.messages.ToolMessages.WARN_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_SKIPPED; |
| | | import static org.opends.server.util.StaticUtils.getFileForPath; |
| | | |
| | | import java.io.ByteArrayOutputStream; |
| | | import java.io.File; |
| | | import java.io.PrintStream; |
| | | import java.nio.file.Files; |
| | | import java.util.ArrayList; |
| | | import java.util.Arrays; |
| | | import java.util.Collections; |
| | | import java.util.HashMap; |
| | | import java.util.HashSet; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | import java.util.TreeMap; |
| | | import java.util.TreeSet; |
| | | |
| | | import javax.security.auth.callback.Callback; |
| | | import javax.security.auth.callback.TextOutputCallback; |
| | | |
| | | import com.forgerock.opendj.cli.ClientException; |
| | | |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.ModificationType; |
| | | import org.forgerock.opendj.ldap.ResultCode; |
| | | import org.forgerock.opendj.ldap.requests.ModifyRequest; |
| | | import org.forgerock.opendj.ldap.requests.Requests; |
| | | import org.opends.server.DirectoryServerTestCase; |
| | | import org.opends.server.TestCaseUtils; |
| | | import org.opends.server.backends.pluggable.IndexKeyRemover; |
| | | import org.opends.server.core.DirectoryServer; |
| | | import org.opends.server.core.ModifyOperation; |
| | | import org.opends.server.tools.VerifyIndex; |
| | | import org.opends.server.types.Entry; |
| | | import org.opends.server.util.StaticUtils; |
| | | import org.testng.annotations.BeforeClass; |
| | | import org.testng.annotations.DataProvider; |
| | | import org.testng.annotations.Test; |
| | | |
| | | import static org.opends.server.protocols.internal.InternalClientConnection.getRootConnection; |
| | | |
| | | /** |
| | | * Tests the upgrade task that verifies the equality indexes of the attributes holding DNs, and rebuilds them where |
| | | * they miss a key (issue #1153). |
| | | */ |
| | | @SuppressWarnings("javadoc") |
| | | public class DNEqualityIndexesUpgradeTestCase extends DirectoryServerTestCase |
| | | { |
| | | private static final String BACKEND_ID = "dnEqualityIndexes"; |
| | | private static final String BACKEND_DN = "ds-cfg-backend-id=" + BACKEND_ID + ",cn=Backends,cn=config"; |
| | | private static final String BASE_DN = "o=dn equality indexes"; |
| | | private static final Set<String> MEMBER = Collections.singleton("member"); |
| | | private static final String MY_MANAGER_AS_DN = |
| | | "( 1.3.6.1.4.1.26027.1.999.1153 NAME 'myManager' SUP distinguishedName )"; |
| | | private static final String MY_MANAGER_AS_STRING = "( 1.3.6.1.4.1.26027.1.999.1153 NAME 'myManager' " |
| | | + "EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )"; |
| | | |
| | | @BeforeClass |
| | | public void setUp() throws Exception |
| | | { |
| | | TestCaseUtils.startServer(); |
| | | } |
| | | |
| | | /** |
| | | * Only the equality indexes whose matching rule compares DNs are found, in enabled pluggable backends; the |
| | | * matching rule of an attribute of a custom schema is read from the schema files. |
| | | */ |
| | | @Test |
| | | public void findsTheEqualityIndexesThatCompareDNs() throws Exception |
| | | { |
| | | final File directory = Files.createTempDirectory("dn-equality-indexes").toFile(); |
| | | try |
| | | { |
| | | final File config = writeConfig(directory, |
| | | backend("userRoot", "userRoot", "true"), |
| | | index("userRoot", "member", "equality"), |
| | | index("userRoot", "uniqueMember", "equality", "presence"), |
| | | index("userRoot", "seeAlso", "Equality"), |
| | | index("userRoot", "myManager", "equality"), |
| | | index("userRoot", "owner", "presence"), |
| | | index("userRoot", "cn", "equality", "substring"), |
| | | // A backend ID that a DN has to escape |
| | | backend("a\\,b", "a,b", "true"), |
| | | index("a\\,b", "member", "equality"), |
| | | // A disabled backend, and one without an index that compares DNs |
| | | backend("disabledRoot", "disabledRoot", "false"), |
| | | index("disabledRoot", "member", "equality"), |
| | | backend("otherRoot", "otherRoot", "true"), |
| | | index("otherRoot", "cn", "equality")); |
| | | final File schemaDirectory = new File(directory, "schema"); |
| | | writeSchema(schemaDirectory, "99-user.ldif", MY_MANAGER_AS_DN); |
| | | |
| | | final Map<String, Set<String>> attributes = |
| | | UpgradeUtils.getDNEqualityIndexedAttributesPerBackend(config, schemaDirectory); |
| | | |
| | | assertThat(attributes.keySet()).containsExactly("a,b", "userRoot"); |
| | | assertThat(attributes.get("userRoot")).containsExactly("member", "myManager", "seeAlso", "uniqueMember"); |
| | | assertThat(attributes.get("a,b")).containsExactly("member"); |
| | | } |
| | | finally |
| | | { |
| | | StaticUtils.recursiveDelete(directory); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * The schema files are read in the order of their names, as the server reads them, so the definition of an |
| | | * attribute in a later file replaces the one in an earlier file. |
| | | */ |
| | | @DataProvider |
| | | public Object[][] schemaFilesInOrder() |
| | | { |
| | | return new Object[][] { |
| | | { MY_MANAGER_AS_STRING, MY_MANAGER_AS_DN, true }, |
| | | { MY_MANAGER_AS_DN, MY_MANAGER_AS_STRING, false }, |
| | | }; |
| | | } |
| | | |
| | | @Test(dataProvider = "schemaFilesInOrder") |
| | | public void aLaterSchemaFileDecidesTheMatchingRule(String first, String last, boolean comparesDNs) throws Exception |
| | | { |
| | | final File directory = Files.createTempDirectory("dn-equality-indexes").toFile(); |
| | | try |
| | | { |
| | | final File config = writeConfig(directory, |
| | | backend("userRoot", "userRoot", "true"), |
| | | index("userRoot", "myManager", "equality")); |
| | | final File schemaDirectory = new File(directory, "schema"); |
| | | // The order of the directory listing depends on the file system, see schemaFilesAreReadInNameOrder |
| | | writeSchema(schemaDirectory, "99-user.ldif", last); |
| | | writeSchema(schemaDirectory, "10-first.ldif", first); |
| | | |
| | | final Map<String, Set<String>> attributes = |
| | | UpgradeUtils.getDNEqualityIndexedAttributesPerBackend(config, schemaDirectory); |
| | | |
| | | if (comparesDNs) |
| | | { |
| | | assertThat(attributes).containsOnlyKeys("userRoot"); |
| | | assertThat(attributes.get("userRoot")).containsExactly("myManager"); |
| | | } |
| | | else |
| | | { |
| | | assertThat(attributes).isEmpty(); |
| | | } |
| | | } |
| | | finally |
| | | { |
| | | StaticUtils.recursiveDelete(directory); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * The schema files are read in the order of their names whatever order the file system lists them in: with 26 |
| | | * files, a directory listing that happens to be sorted is unlikely. |
| | | */ |
| | | @Test |
| | | public void schemaFilesAreReadInNameOrder() throws Exception |
| | | { |
| | | final File directory = Files.createTempDirectory("dn-equality-indexes").toFile(); |
| | | try |
| | | { |
| | | for (char c = 'z'; c >= 'a'; c--) |
| | | { |
| | | assertThat(new File(directory, "50-" + c + ".ldif").createNewFile()).isTrue(); |
| | | } |
| | | assertThat(new File(directory, "50-not-a-schema-file.txt").createNewFile()).isTrue(); |
| | | |
| | | final List<String> names = new ArrayList<>(); |
| | | for (final File file : UpgradeUtils.schemaFilesInReadOrder(directory)) |
| | | { |
| | | names.add(file.getName()); |
| | | } |
| | | |
| | | assertThat(names).hasSize(26).isSorted(); |
| | | } |
| | | finally |
| | | { |
| | | StaticUtils.recursiveDelete(directory); |
| | | } |
| | | } |
| | | |
| | | /** Each base DN gets the attributes of its backend; a backend without known base DNs is left out. */ |
| | | @Test |
| | | public void eachBaseDNGetsTheIndexesOfItsBackend() |
| | | { |
| | | final Map<String, Set<String>> attributesPerBackend = new HashMap<>(); |
| | | attributesPerBackend.put("a,b", Collections.singleton("member")); |
| | | attributesPerBackend.put("c", Collections.singleton("uniqueMember")); |
| | | final Map<String, Set<String>> baseDNsPerBackend = new HashMap<>(); |
| | | baseDNsPerBackend.put("a,b", new HashSet<>(Arrays.asList("o=a", "o=b"))); |
| | | baseDNsPerBackend.put("d", Collections.singleton("o=d")); |
| | | |
| | | final Map<String, Set<String>> indexes = |
| | | UpgradeTasks.getDNEqualityIndexesToVerify(attributesPerBackend, baseDNsPerBackend); |
| | | |
| | | assertThat(indexes).containsOnlyKeys("o=a", "o=b"); |
| | | assertThat(indexes.get("o=a")).containsExactly("member"); |
| | | assertThat(indexes.get("o=b")).containsExactly("member"); |
| | | } |
| | | |
| | | private static File writeConfig(File directory, String... entries) throws Exception |
| | | { |
| | | final File config = new File(directory, "config.ldif"); |
| | | Files.write(config.toPath(), String.join("\n", |
| | | "dn: cn=config", |
| | | "objectClass: top", |
| | | "objectClass: ds-cfg-root-config", |
| | | "cn: config", |
| | | "", |
| | | "dn: cn=Backends,cn=config", |
| | | "objectClass: top", |
| | | "objectClass: ds-cfg-branch", |
| | | "cn: Backends", |
| | | "", |
| | | String.join("\n", entries)).getBytes(UTF_8)); |
| | | return config; |
| | | } |
| | | |
| | | private static void writeSchema(File schemaDirectory, String fileName, String attributeType) throws Exception |
| | | { |
| | | schemaDirectory.mkdir(); |
| | | Files.write(new File(schemaDirectory, fileName).toPath(), String.join("\n", |
| | | "dn: cn=schema", |
| | | "objectClass: top", |
| | | "objectClass: ldapSubentry", |
| | | "objectClass: subschema", |
| | | "attributeTypes: " + attributeType, |
| | | "").getBytes(UTF_8)); |
| | | } |
| | | |
| | | private static String backend(String rdnValue, String backendID, String enabled) |
| | | { |
| | | return String.join("\n", |
| | | "dn: ds-cfg-backend-id=" + rdnValue + ",cn=Backends,cn=config", |
| | | "objectClass: top", |
| | | "objectClass: ds-cfg-backend", |
| | | "objectClass: ds-cfg-pluggable-backend", |
| | | "objectClass: ds-cfg-pdb-backend", |
| | | "ds-cfg-backend-id: " + backendID, |
| | | "ds-cfg-enabled: " + enabled, |
| | | "ds-cfg-base-dn: o=" + backendID.replace(",", "\\,"), |
| | | "", |
| | | "dn: cn=Index,ds-cfg-backend-id=" + rdnValue + ",cn=Backends,cn=config", |
| | | "objectClass: top", |
| | | "objectClass: ds-cfg-branch", |
| | | "cn: Index", |
| | | ""); |
| | | } |
| | | |
| | | private static String index(String backendRDNValue, String attribute, String... indexTypes) |
| | | { |
| | | final StringBuilder entry = new StringBuilder() |
| | | .append("dn: ds-cfg-attribute=").append(attribute) |
| | | .append(",cn=Index,ds-cfg-backend-id=").append(backendRDNValue).append(",cn=Backends,cn=config\n") |
| | | .append("objectClass: top\n") |
| | | .append("objectClass: ds-cfg-backend-index\n") |
| | | .append("ds-cfg-attribute: ").append(attribute).append('\n'); |
| | | for (String indexType : indexTypes) |
| | | { |
| | | entry.append("ds-cfg-index-type: ").append(indexType).append('\n'); |
| | | } |
| | | return entry.append('\n').toString(); |
| | | } |
| | | |
| | | /** |
| | | * A trusted equality index that misses the keys of its entries is rebuilt, and an index that matches its |
| | | * entries is only verified. |
| | | */ |
| | | @Test |
| | | public void rebuildsAnIndexOnlyWhenItMissesKeys() throws Exception |
| | | { |
| | | withAGroupWhoseMemberIndexMissesItsKeys("db_dn_equality_indexes", (configFile, out, output) -> { |
| | | final List<TextOutputCallback> rebuildNotifications = new ArrayList<>(); |
| | | UpgradeTasks.verifyAndRebuildOrWarn(newContext(rebuildNotifications), configFile, BASE_DN, MEMBER, false, out); |
| | | assertThat(messagesOf(rebuildNotifications)).as("output: %s", output) |
| | | .contains(INFO_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_INCONSISTENT.get("member", BASE_DN).toString()); |
| | | // The rebuild closed `out`: the calls after it write to a stream of their own |
| | | final PrintStream afterRebuild = new PrintStream(output, true, UTF_8); |
| | | assertThat(UpgradeTasks.verifyAndRebuildIndexes(configFile, BASE_DN, MEMBER, false, afterRebuild)) |
| | | .as("verification of the rebuilt index; output: %s", output) |
| | | .isEqualTo(UpgradeTasks.IndexVerification.CONSISTENT); |
| | | |
| | | final List<TextOutputCallback> notifications = new ArrayList<>(); |
| | | UpgradeTasks.verifyAndRebuildOrWarn(newContext(notifications), configFile, BASE_DN, MEMBER, false, |
| | | afterRebuild); |
| | | assertThat(messagesOf(notifications)) |
| | | .contains(INFO_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_CONSISTENT.get("member", BASE_DN).toString()) |
| | | .doesNotContain(INFO_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_INCONSISTENT.get("member", BASE_DN).toString()) |
| | | .doesNotContain(WARN_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_FAILED.get("member", BASE_DN).toString()); |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * A rebuild that fails, after the verification found missing keys, fails the upgrade: the rebuild may have left |
| | | * the indexes untrusted, so that searches cannot use them, and a warning would let the upgrade succeed. |
| | | */ |
| | | @Test |
| | | public void aRebuildThatFailsFailsTheUpgrade() throws Exception |
| | | { |
| | | withAGroupWhoseMemberIndexMissesItsKeys("db_dn_equality_indexes_failed_rebuild", (configFile, out, output) -> |
| | | withTheRebuildFailing(() -> { |
| | | final List<TextOutputCallback> notifications = new ArrayList<>(); |
| | | final Throwable failure = catchThrowable(() -> UpgradeTasks.verifyAndRebuildOrWarn( |
| | | newContext(notifications), configFile, BASE_DN, MEMBER, false, out)); |
| | | |
| | | assertThat(failure).as("output: %s", output).isInstanceOf(ClientException.class); |
| | | assertThat(output.toString("UTF-8")).as("the rebuild failed, not the verification") |
| | | .contains("An error occurs during the rebuild index process in " + BASE_DN); |
| | | assertThat(messagesOf(notifications)) |
| | | .doesNotContain(WARN_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_FAILED.get("member", BASE_DN).toString()); |
| | | })); |
| | | } |
| | | |
| | | /** |
| | | * After a rebuild that failed, the indexes of the next base DNs are neither verified nor rebuilt, since the cause |
| | | * of the failure, such as a full temporary directory, would most likely make their rebuild fail too, after it had |
| | | * deleted them: the user is told which indexes were left, and the upgrade fails. |
| | | */ |
| | | @Test |
| | | public void aFailedRebuildLeavesTheIndexesOfTheNextBaseDNsUnverified() throws Exception |
| | | { |
| | | withAGroupWhoseMemberIndexMissesItsKeys("db_dn_equality_indexes_next_base_dns", (configFile, out, output) -> |
| | | withTheRebuildFailing(() -> { |
| | | final String nextBaseDN = "o=held by no backend"; |
| | | final Map<String, Set<String>> indexesPerBaseDN = new TreeMap<>(); |
| | | indexesPerBaseDN.put(nextBaseDN, MEMBER); |
| | | indexesPerBaseDN.put(BASE_DN, MEMBER); |
| | | assertThat(indexesPerBaseDN.keySet()).as("the rebuild fails first").containsExactly(BASE_DN, nextBaseDN); |
| | | |
| | | final List<TextOutputCallback> notifications = new ArrayList<>(); |
| | | final Throwable failure = catchThrowable(() -> UpgradeTasks.verifyAndRebuildOrWarn(newContext(notifications), |
| | | configFile, indexesPerBaseDN, false, () -> new PrintStream(output, true, UTF_8))); |
| | | |
| | | assertThat(failure).as("output: %s", output).isInstanceOf(ClientException.class); |
| | | assertThat(messagesOf(notifications)) |
| | | .contains(WARN_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_SKIPPED.get("member", nextBaseDN).toString()) |
| | | .doesNotContain(INFO_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_STARTS.get("member", nextBaseDN).toString()) |
| | | .doesNotContain(WARN_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_SKIPPED.get("member", BASE_DN).toString()); |
| | | })); |
| | | } |
| | | |
| | | /** |
| | | * Indexes under a base DN that no backend holds cannot be verified: they are not rebuilt, and do not fail the |
| | | * upgrade, the user is warned instead. |
| | | */ |
| | | @Test |
| | | public void indexesThatCannotBeVerifiedOnlyWarn() throws Exception |
| | | { |
| | | final String baseDN = "o=held by no backend"; |
| | | final ByteArrayOutputStream output = new ByteArrayOutputStream(); |
| | | final PrintStream out = new PrintStream(output, true, "UTF-8"); |
| | | final String configFile = DirectoryServer.getConfigFile(); |
| | | assertThat(UpgradeTasks.verifyAndRebuildIndexes(configFile, baseDN, MEMBER, false, out)) |
| | | .as("output: %s", output).isEqualTo(UpgradeTasks.IndexVerification.NOT_VERIFIED); |
| | | |
| | | final List<TextOutputCallback> notifications = new ArrayList<>(); |
| | | UpgradeTasks.verifyAndRebuildOrWarn(newContext(notifications), configFile, baseDN, MEMBER, false, out); |
| | | |
| | | final String warning = WARN_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_FAILED.get("member", baseDN).toString(); |
| | | assertThat(messagesOf(notifications)).contains(warning); |
| | | for (final TextOutputCallback notification : notifications) |
| | | { |
| | | if (notification.getMessage().equals(warning)) |
| | | { |
| | | assertThat(((FormattedNotificationCallback) notification).getMessageSubType()) |
| | | .isEqualTo(TextOutputCallback.WARNING); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Indexes that their backend fails to verify, as a JDBC or Cassandra backend whose database is down does, are |
| | | * left as they were, and the user is warned. Here the backend fails on an index that it does not have. |
| | | */ |
| | | @Test |
| | | public void indexesThatTheBackendFailsToVerifyAreLeftAsTheyWere() throws Exception |
| | | { |
| | | withAGroupWhoseMemberIndexMissesItsKeys("db_dn_equality_indexes_failed_verify", (configFile, out, output) -> { |
| | | final Set<String> attributes = new TreeSet<>(Arrays.asList("member", "uniqueMember")); |
| | | final List<TextOutputCallback> notifications = new ArrayList<>(); |
| | | UpgradeTasks.verifyAndRebuildOrWarn(newContext(notifications), configFile, BASE_DN, attributes, false, out); |
| | | |
| | | assertThat(messagesOf(notifications)).as("output: %s", output) |
| | | .contains(WARN_UPGRADE_VERIFY_DN_EQUALITY_INDEXES_FAILED.get("member, uniqueMember", BASE_DN).toString()); |
| | | final String[] verifyMember = { "--configFile", configFile, "--baseDN", BASE_DN, "--index", "member" }; |
| | | assertThat(VerifyIndex.countIndexErrors(verifyMember, false, out)) |
| | | .as("the member index still misses its keys; output: %s", output).isPositive(); |
| | | }); |
| | | } |
| | | |
| | | /** What a test runs against the backend of {@link #withAGroupWhoseMemberIndexMissesItsKeys}. */ |
| | | private interface IndexAction |
| | | { |
| | | void run(String configFile, PrintStream out, ByteArrayOutputStream output) throws Exception; |
| | | } |
| | | |
| | | /** What a test runs while {@link #withTheRebuildFailing} makes the rebuilds fail. */ |
| | | private interface RebuildAction |
| | | { |
| | | void run() throws Exception; |
| | | } |
| | | |
| | | /** Runs an action while a file where the rebuild creates its temporary directory makes every rebuild fail. */ |
| | | private static void withTheRebuildFailing(RebuildAction action) throws Exception |
| | | { |
| | | final File tmpDirectory = getFileForPath("import-tmp"); |
| | | final File setAside = new File(tmpDirectory.getPath() + ".set-aside"); |
| | | final boolean existed = tmpDirectory.exists(); |
| | | if (existed) |
| | | { |
| | | assertThat(tmpDirectory.renameTo(setAside)).isTrue(); |
| | | } |
| | | try |
| | | { |
| | | assertThat(tmpDirectory.createNewFile()).isTrue(); |
| | | action.run(); |
| | | } |
| | | finally |
| | | { |
| | | tmpDirectory.delete(); |
| | | if (existed) |
| | | { |
| | | assertThat(setAside.renameTo(tmpDirectory)).isTrue(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Runs an action against a disabled PDB backend holding a group whose trusted member equality index misses the |
| | | * keys of its entries, then removes the backend. Each test gives its own database directory, since removing the |
| | | * backend leaves its database behind. |
| | | */ |
| | | private static void withAGroupWhoseMemberIndexMissesItsKeys(String dbDirectory, IndexAction action) |
| | | throws Exception |
| | | { |
| | | final Entry backend = TestCaseUtils.makeEntry( |
| | | "dn: " + BACKEND_DN, |
| | | "objectClass: top", |
| | | "objectClass: ds-cfg-backend", |
| | | "objectClass: ds-cfg-local-backend", |
| | | "objectClass: ds-cfg-pluggable-backend", |
| | | "objectClass: ds-cfg-pdb-backend", |
| | | "ds-cfg-enabled: true", |
| | | "ds-cfg-java-class: org.opends.server.backends.pdb.PDBBackend", |
| | | "ds-cfg-backend-id: " + BACKEND_ID, |
| | | "ds-cfg-writability-mode: enabled", |
| | | "ds-cfg-base-dn: " + BASE_DN, |
| | | "ds-cfg-db-directory: " + dbDirectory, |
| | | "ds-cfg-db-cache-percent: 2"); |
| | | final Entry indexBranch = TestCaseUtils.makeEntry( |
| | | "dn: cn=Index," + BACKEND_DN, |
| | | "objectClass: top", |
| | | "objectClass: ds-cfg-branch", |
| | | "cn: Index"); |
| | | final Entry memberIndex = TestCaseUtils.makeEntry( |
| | | "dn: ds-cfg-attribute=member,cn=Index," + BACKEND_DN, |
| | | "objectClass: top", |
| | | "objectClass: ds-cfg-backend-index", |
| | | "ds-cfg-attribute: member", |
| | | "ds-cfg-index-type: equality"); |
| | | TestCaseUtils.addEntry(backend); |
| | | try |
| | | { |
| | | TestCaseUtils.addEntry(indexBranch); |
| | | TestCaseUtils.addEntry(memberIndex); |
| | | TestCaseUtils.addEntries( |
| | | "dn: " + BASE_DN, |
| | | "objectClass: top", |
| | | "objectClass: organization", |
| | | "o: dn equality indexes", |
| | | "", |
| | | "dn: cn=group," + BASE_DN, |
| | | "objectClass: top", |
| | | "objectClass: groupOfNames", |
| | | "cn: group", |
| | | "member: cn=a," + BASE_DN, |
| | | "member: cn=b;" + BASE_DN); |
| | | // A trusted index without the keys of these entries, as one whose keys a previous version computed |
| | | // differently |
| | | IndexKeyRemover.removeAllKeys( |
| | | DirectoryServer.getInstance().getServerContext().getBackendConfigManager().getLocalBackendById(BACKEND_ID), |
| | | DN.valueOf(BASE_DN), "member"); |
| | | setBackendEnabled(false); |
| | | |
| | | final ByteArrayOutputStream output = new ByteArrayOutputStream(); |
| | | action.run(DirectoryServer.getConfigFile(), new PrintStream(output, true, "UTF-8"), output); |
| | | } |
| | | finally |
| | | { |
| | | // A failed setup or test must not be hidden by the cleanup, nor leave the backend configured for the next |
| | | // tests: the backend may not take the change, as when a tool run in this process left it half initialized |
| | | getRootConnection().processModify(enableBackend(false)); |
| | | for (final DN dn : Arrays.asList(memberIndex.getName(), indexBranch.getName(), backend.getName())) |
| | | { |
| | | if (DirectoryServer.entryExists(dn)) |
| | | { |
| | | TestCaseUtils.deleteEntry(dn); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** Returns an upgrade context that records what it notifies. */ |
| | | private static UpgradeContext newContext(final List<TextOutputCallback> notifications) throws Exception |
| | | { |
| | | return new UpgradeContext(callbacks -> { |
| | | for (final Callback callback : callbacks) |
| | | { |
| | | if (callback instanceof TextOutputCallback) |
| | | { |
| | | notifications.add((TextOutputCallback) callback); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private static List<String> messagesOf(final List<TextOutputCallback> notifications) |
| | | { |
| | | final List<String> messages = new ArrayList<>(); |
| | | for (final TextOutputCallback notification : notifications) |
| | | { |
| | | messages.add(notification.getMessage()); |
| | | } |
| | | return messages; |
| | | } |
| | | |
| | | private static void setBackendEnabled(boolean enabled) |
| | | { |
| | | final ModifyOperation modifyOperation = getRootConnection().processModify(enableBackend(enabled)); |
| | | assertThat(modifyOperation.getResultCode()).isEqualTo(ResultCode.SUCCESS); |
| | | } |
| | | |
| | | private static ModifyRequest enableBackend(boolean enabled) |
| | | { |
| | | return Requests.newModifyRequest(BACKEND_DN) |
| | | .addModification(ModificationType.REPLACE, "ds-cfg-enabled", Boolean.toString(enabled)); |
| | | } |
| | | } |
| | |
| | | package org.opends.server.types; |
| | | |
| | | import static org.testng.Assert.assertEquals; |
| | | import static org.testng.Assert.assertTrue; |
| | | |
| | | import org.forgerock.opendj.ldap.SearchScope; |
| | | import org.opends.server.TestCaseUtils; |
| | | import org.testng.annotations.BeforeClass; |
| | | import org.testng.annotations.DataProvider; |
| | |
| | | LDAPURL.decode(urlString, true); |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Test data for testNonAsciiBaseDNIsPercentEncodedAsUTF8. |
| | | * |
| | | * @return DNs with non-ASCII characters and their percent-encoded UTF-8 form. |
| | | */ |
| | | @DataProvider |
| | | public Object[][] nonAsciiBaseDNData() |
| | | { |
| | | return new Object[][] { |
| | | { "cn=J\u00f6rg \u0416,ou=Remote,dc=example,dc=com", |
| | | "cn=J%C3%B6rg%20%D0%96,ou=Remote,dc=example,dc=com" }, |
| | | // A character outside the BMP is one code point made of two Java chars |
| | | { "cn=\uD83D\uDE00,dc=x", "cn=%F0%9F%98%80,dc=x" }, |
| | | }; |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * A referral URL percent-encodes the UTF-8 octets of the DN (RFC 4516 section 2.1), so that |
| | | * {@link LDAPURL#decode(String, boolean)} gives back the same DN - see issue #1153. |
| | | * |
| | | * @param dn |
| | | * The base DN. |
| | | * @param encodedDN |
| | | * The expected percent-encoded base DN. |
| | | * @throws Exception |
| | | * If an unexpected exception occurred. |
| | | */ |
| | | @Test(dataProvider = "nonAsciiBaseDNData") |
| | | public void testNonAsciiBaseDNIsPercentEncodedAsUTF8(String dn, String encodedDN) throws Exception |
| | | { |
| | | LDAPURL url = new LDAPURL("ldap", "other.example.com", 389, dn, null, SearchScope.BASE_OBJECT, null, null); |
| | | String urlString = url.toString(); |
| | | assertTrue(urlString.startsWith("ldap://other.example.com:389/" + encodedDN + "?"), urlString); |
| | | assertEquals(LDAPURL.decode(urlString, true).getRawBaseDN(), dn); |
| | | } |
| | | |
| | | } |