diff --git a/opendj-doc-generated-ref/src/main/asciidoc/server-dev-guide/chap-groups.adoc b/opendj-doc-generated-ref/src/main/asciidoc/server-dev-guide/chap-groups.adoc index e0c248c6a3..e60b8751a8 100644 --- a/opendj-doc-generated-ref/src/main/asciidoc/server-dev-guide/chap-groups.adoc +++ b/opendj-doc-generated-ref/src/main/asciidoc/server-dev-guide/chap-groups.adoc @@ -12,7 +12,7 @@ information: "Portions copyright [year] [name of copyright owner]". Copyright 2017 ForgeRock AS. - Portions Copyright 2024 3A Systems LLC. + Portions Copyright 2024-2026 3A Systems, LLC. //// :figure-caption!: @@ -539,6 +539,6 @@ member: uid=tmorris,ou=People,dc=example,dc=com ---- By default, the referential integrity plugin is configured to manage `member` and `uniqueMember` attributes. These attributes take values that are DNs, and are indexed for equality by default for the default backend. Before you add an additional attribute to manage, make sure that it has DN syntax and that it is indexed for equality. OpenDJ directory server requires that the attribute be indexed because an unindexed search for integrity would potentially consume too many of the server's resources. Attribute syntax is explained in xref:../admin-guide/chap-schema.adoc#chap-schema["Managing Schema"] in the __Administration Guide__. For instructions on indexing attributes, see xref:../admin-guide/chap-indexing.adoc#configure-indexes["Configuring and Rebuilding Indexes"] in the __Administration Guide__. -You can also configure the referential integrity plugin to check that new entries added to groups actually exist in the directory by setting the `check-references` property to `true`. You can specify additional criteria once you have activated the check. To ensure that entries added must match a filter, set the `check-references-filter-criteria` to identify the attribute and the filter. For example, you can specify that group members must be person entries by setting `check-references-filter-criteria` to `member:(objectclass=person)`. To ensure that entries must be located in the same naming context, set `check-references-scope-criteria` to `naming-context`. +You can also configure the referential integrity plugin to check that new entries added to groups actually exist in the directory by setting the `check-references` property to `true`. You can specify additional criteria once you have activated the check. To ensure that entries added must match a filter, set the `check-references-filter-criteria` to identify the attribute and the filter. For example, you can specify that group members must be person entries by setting `check-references-filter-criteria` to `member:(objectclass=person)`. To ensure that entries must be located in the same naming context, set `check-references-scope-criteria` to `naming-context`. The check runs when entries are added and modified, so the plugin must be registered for the `preOperationAdd` and `preOperationModify` plugin types, as the default configuration is. When the plugin is enabled, OpenDJ refuses to set `check-references` to `true` if `plugin-type` lacks either of them, and it refuses to enable a plugin configured that way. A plugin already configured that way when the server starts is loaded with a warning, and it does not check references until the types are added. Plugin types take effect when the plugin is enabled, so add them before enabling the plugin, or disable and re-enable it afterwards. diff --git a/opendj-server-legacy/resource/config/config.ldif b/opendj-server-legacy/resource/config/config.ldif index ccf0fbf3ca..4079bc88ef 100644 --- a/opendj-server-legacy/resource/config/config.ldif +++ b/opendj-server-legacy/resource/config/config.ldif @@ -1388,6 +1388,8 @@ ds-cfg-plugin-type: postOperationDelete ds-cfg-plugin-type: postOperationModifyDN ds-cfg-plugin-type: subordinateModifyDN ds-cfg-plugin-type: subordinateDelete +ds-cfg-plugin-type: preOperationAdd +ds-cfg-plugin-type: preOperationModify ds-cfg-attribute-type: member ds-cfg-attribute-type: uniqueMember ds-cfg-invoke-for-internal-operations: true diff --git a/opendj-server-legacy/src/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java b/opendj-server-legacy/src/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java index 1b2a5bc537..f42d845141 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java @@ -33,6 +33,7 @@ import java.io.FileWriter; import java.io.IOException; import java.util.Collections; +import java.util.EnumSet; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -105,6 +106,9 @@ public class ReferentialIntegrityPlugin { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); + /** The plugin types {@code check-references} needs: the references are checked in these hooks. */ + private static final Set CHECK_REFERENCES_PLUGIN_TYPES = Collections.unmodifiableSet( + EnumSet.of(PluginCfgDefn.PluginType.PREOPERATIONADD, PluginCfgDefn.PluginType.PREOPERATIONMODIFY)); /** Current plugin configuration. */ @@ -169,11 +173,19 @@ public final void initializePlugin(Set pluginTypes, pluginCfg.addReferentialIntegrityChangeListener(this); LinkedList unacceptableReasons = new LinkedList<>(); - if (!isConfigurationAcceptable(pluginCfg, unacceptableReasons)) + if (!isConfigurationAcceptableIgnoringCheckReferencesPluginTypes(pluginCfg, unacceptableReasons)) { throw new ConfigException(unacceptableReasons.getFirst()); } + // Enabling the plugin or changing its configuration is refused without these types, but a configuration + // already stored without them (the entry shipped before issue #1118) is loaded with a warning: refusing + // it would also stop the delete and modify DN clean-up, which does not need them. + for (PluginCfgDefn.PluginType t : getMissingCheckReferencesPluginTypes(pluginCfg)) + { + logger.warn(WARN_PLUGIN_REFERENT_CHECK_REFERENCES_WITHOUT_PLUGIN_TYPE.get(pluginCfg.dn(), t, t)); + } + applyConfigurationChange(pluginCfg); // Set up log file. Note: it is not allowed to change once the plugin is active. @@ -255,9 +267,39 @@ public ConfigChangeResult applyConfigurationChange( public boolean isConfigurationAcceptable(PluginCfg configuration, List unacceptableReasons) { - boolean isAcceptable = true; ReferentialIntegrityPluginCfg pluginCfg = (ReferentialIntegrityPluginCfg) configuration; + boolean isAcceptable = isConfigurationAcceptableIgnoringCheckReferencesPluginTypes(pluginCfg, unacceptableReasons); + + for (PluginCfgDefn.PluginType t : getMissingCheckReferencesPluginTypes(pluginCfg)) + { + isAcceptable = false; + unacceptableReasons.add(ERR_PLUGIN_REFERENT_CHECK_REFERENCES_WITHOUT_PLUGIN_TYPE.get(t, t)); + } + return isAcceptable; + } + + /** + * Returns the plugin types {@code check-references} needs that the configuration does not list. The references + * are checked in the pre-operation add and modify hooks, which the plugin manager only calls for the plugin types + * listed in the configuration. + */ + private static Set getMissingCheckReferencesPluginTypes( + ReferentialIntegrityPluginCfg pluginCfg) + { + if (!pluginCfg.isCheckReferences()) + { + return Collections.emptySet(); + } + Set missing = EnumSet.copyOf(CHECK_REFERENCES_PLUGIN_TYPES); + missing.removeAll(pluginCfg.getPluginType()); + return missing; + } + + private boolean isConfigurationAcceptableIgnoringCheckReferencesPluginTypes( + ReferentialIntegrityPluginCfg pluginCfg, List unacceptableReasons) + { + boolean isAcceptable = true; for (PluginCfgDefn.PluginType t : pluginCfg.getPluginType()) { diff --git a/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/Upgrade.java b/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/Upgrade.java index 10b0590a1c..f20ba21cf9 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/Upgrade.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/Upgrade.java @@ -97,6 +97,12 @@ public final class Upgrade "cn: End Transaction", "ds-cfg-java-class: org.opends.server.extensions.EndTransactionExtendedOperation", "ds-cfg-enabled: true" }; + static final String REFERENTIAL_INTEGRITY_PLUGIN_FILTER = + "(objectClass=ds-cfg-referential-integrity-plugin)"; + static final String[] ADD_REFERENTIAL_INTEGRITY_PRE_OPERATION_PLUGIN_TYPES = { + "add: ds-cfg-plugin-type", + "ds-cfg-plugin-type: preOperationAdd", + "ds-cfg-plugin-type: preOperationModify" }; static { @@ -638,6 +644,16 @@ public String toString() { START_TRANSACTION_HANDLER_ENTRY), addConfigEntry(END_TRANSACTION_HANDLER_ENTRY)); + /* See issue #1118: the shipped Referential Integrity plugin entry lacked the pre-operation plugin + * types that check-references runs in, so turning check-references on checked nothing. The plugin + * now refuses check-references without them, so every referential integrity plugin entry gets them: + * the add is permissive, and the upgrade schema matches plugin types ignoring case, so a type an + * administrator already added is not added a second time. */ + register("5.2.0", + modifyConfigEntry(INFO_UPGRADE_TASK_ADD_REFERENTIAL_INTEGRITY_PRE_OPERATION_PLUGIN_TYPES.get(), + REFERENTIAL_INTEGRITY_PLUGIN_FILTER, + ADD_REFERENTIAL_INTEGRITY_PRE_OPERATION_PLUGIN_TYPES)); + /* * 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/. @@ -763,7 +779,7 @@ private static UpgradeTask convertJEBackendsToPDBBackends(final String objectCla * @return A list containing all the tasks which are required in order to upgrade * from {@code fromVersion} to {@code toVersion}. */ - private static List getUpgradeTasks(final BuildVersion fromVersion, final BuildVersion toVersion) + static List getUpgradeTasks(final BuildVersion fromVersion, final BuildVersion toVersion) { final List tasks = new LinkedList<>(); try { diff --git a/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/UpgradeUtils.java b/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/UpgradeUtils.java index c7f43fe925..e9be4cb753 100644 --- a/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/UpgradeUtils.java +++ b/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/UpgradeUtils.java @@ -728,6 +728,12 @@ private static Schema getUpgradeSchema() + " EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15" + " X-ORIGIN 'OpenDS Directory Server' )", false); + // Adds ds-cfg-plugin-type / ignore match syntax: dsconfig writes plugin types in lower case, + // while config.ldif spells them in camel case (issue #1118) + sb.addAttributeType("( 1.3.6.1.4.1.26027.1.1.54 NAME 'ds-cfg-plugin-type'" + + " EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15" + + " X-ORIGIN 'OpenDS Directory Server' )", false); + return sb.toSchema().asNonStrictSchema(); } diff --git a/opendj-server-legacy/src/messages/org/opends/messages/plugin.properties b/opendj-server-legacy/src/messages/org/opends/messages/plugin.properties index 9913479a2d..e114683693 100644 --- a/opendj-server-legacy/src/messages/org/opends/messages/plugin.properties +++ b/opendj-server-legacy/src/messages/org/opends/messages/plugin.properties @@ -354,3 +354,13 @@ ERR_PLUGIN_REFERENT_NAMINGCONTEXT_MISMATCH_128=The entry referenced by the \ the configured naming contexts ERR_PLUGIN_REFERENT_EXCEPTION_129=The opration could not be processed \ due to an unexpected exception: '%s' +ERR_PLUGIN_REFERENT_CHECK_REFERENCES_WITHOUT_PLUGIN_TYPE_131=The property \ + 'check-references' is set to true, but the property 'plugin-type' does not list \ + '%s', so the references added by that operation would not be checked. Add '%s' to \ + 'plugin-type', then disable and re-enable the plugin, or set 'check-references' to false +WARN_PLUGIN_REFERENT_CHECK_REFERENCES_WITHOUT_PLUGIN_TYPE_132=The Referential \ + Integrity plugin %s has 'check-references' set to true, but its property \ + 'plugin-type' does not list '%s', so the references added by that operation are \ + not checked. The plugin is loaded and still removes the references to deleted and \ + renamed entries. Add '%s' to 'plugin-type', then disable and re-enable the plugin, \ + or set 'check-references' to false diff --git a/opendj-server-legacy/src/messages/org/opends/messages/tool.properties b/opendj-server-legacy/src/messages/org/opends/messages/tool.properties index 34d2f5e210..1c87085a3c 100644 --- a/opendj-server-legacy/src/messages/org/opends/messages/tool.properties +++ b/opendj-server-legacy/src/messages/org/opends/messages/tool.properties @@ -2738,3 +2738,5 @@ INFO_UPGRADE_TASK_ADD_SUBORDINATE_BASE_DN_TO_GLOBAL_CONFIG=Adding subordinate-ba Global configuration INFO_UPGRADE_TASK_ADD_TRANSACTION_EXTENDED_OPERATIONS=Adding configuration for LDAP transactions \ 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 diff --git a/opendj-server-legacy/src/test/java/org/opends/server/plugins/ReferentialIntegrityPluginTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/plugins/ReferentialIntegrityPluginTestCase.java index 068e30833a..580f328097 100644 --- a/opendj-server-legacy/src/test/java/org/opends/server/plugins/ReferentialIntegrityPluginTestCase.java +++ b/opendj-server-legacy/src/test/java/org/opends/server/plugins/ReferentialIntegrityPluginTestCase.java @@ -18,8 +18,13 @@ */ package org.opends.server.plugins; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; import java.util.List; +import org.forgerock.i18n.LocalizableMessage; import org.forgerock.opendj.config.server.ConfigException; import org.forgerock.opendj.ldap.ByteString; import org.forgerock.opendj.ldap.DN; @@ -30,6 +35,7 @@ import org.forgerock.opendj.ldap.requests.Requests; import org.forgerock.opendj.ldap.schema.AttributeType; import org.opends.server.TestCaseUtils; +import org.forgerock.opendj.server.config.meta.PluginCfgDefn.PluginType; import org.forgerock.opendj.server.config.meta.ReferentialIntegrityPluginCfgDefn; import org.opends.server.api.Group; import org.opends.server.controls.SubtreeDeleteControl; @@ -54,6 +60,7 @@ import org.testng.annotations.Test; import static org.forgerock.opendj.ldap.ModificationType.*; +import static org.opends.messages.PluginMessages.*; import static org.opends.server.core.DirectoryServer.*; import static org.opends.server.protocols.internal.InternalClientConnection.*; import static org.opends.server.protocols.internal.Requests.*; @@ -781,6 +788,215 @@ public void testInitializeWithInValidConfigs(Entry e) plugin.finalizePlugin(); } + /** + * Issue #1118: configurations with {@code check-references} set to true that lack a pre-operation plugin type, + * with the plugin types each one lacks. + */ + @DataProvider(name = "checkReferencesWithoutPreOperationTypes") + public Object[][] createCheckReferencesWithoutPreOperationTypes() + throws Exception + { + List entries = TestCaseUtils.makeEntries( + // check-references true, plugin types of the entry shipped before issue #1118 + "dn: cn=Referential Integrity,cn=Plugins,cn=config", + "objectClass: top", + "objectClass: ds-cfg-plugin", + "objectClass: ds-cfg-referential-integrity-plugin", + "cn: Referential Integrity", + "ds-cfg-java-class: org.opends.server.plugins.ReferentialIntegrityPlugin", + "ds-cfg-enabled: true", + "ds-cfg-plugin-type: postOperationDelete", + "ds-cfg-plugin-type: postOperationModifyDN", + "ds-cfg-plugin-type: subordinateModifyDN", + "ds-cfg-plugin-type: subordinateDelete", + "ds-cfg-attribute-type: member", + "ds-cfg-check-references: true", + "", + // check-references true, preOperationModify missing + "dn: cn=Referential Integrity,cn=Plugins,cn=config", + "objectClass: top", + "objectClass: ds-cfg-plugin", + "objectClass: ds-cfg-referential-integrity-plugin", + "cn: Referential Integrity", + "ds-cfg-java-class: org.opends.server.plugins.ReferentialIntegrityPlugin", + "ds-cfg-enabled: true", + "ds-cfg-plugin-type: postOperationDelete", + "ds-cfg-plugin-type: preOperationAdd", + "ds-cfg-attribute-type: member", + "ds-cfg-check-references: true", + "", + // check-references true, preOperationAdd missing + "dn: cn=Referential Integrity,cn=Plugins,cn=config", + "objectClass: top", + "objectClass: ds-cfg-plugin", + "objectClass: ds-cfg-referential-integrity-plugin", + "cn: Referential Integrity", + "ds-cfg-java-class: org.opends.server.plugins.ReferentialIntegrityPlugin", + "ds-cfg-enabled: true", + "ds-cfg-plugin-type: postOperationDelete", + "ds-cfg-plugin-type: preOperationModify", + "ds-cfg-attribute-type: member", + "ds-cfg-check-references: true" + ); + return new Object[][] { + { entries.get(0), new PluginType[] { PluginType.PREOPERATIONADD, PluginType.PREOPERATIONMODIFY } }, + { entries.get(1), new PluginType[] { PluginType.PREOPERATIONMODIFY } }, + { entries.get(2), new PluginType[] { PluginType.PREOPERATIONADD } }, + }; + } + + /** + * Issue #1118: enabling the plugin or changing its configuration is refused when + * {@code check-references} lacks a pre-operation plugin type, with one reason for each missing type. + */ + @Test(dataProvider = "checkReferencesWithoutPreOperationTypes") + public void testCheckReferencesWithoutPreOperationTypesIsNotAcceptable(Entry e, PluginType[] missing) + throws Exception + { + List expected = new ArrayList<>(); + for (PluginType t : missing) + { + expected.add(ERR_PLUGIN_REFERENT_CHECK_REFERENCES_WITHOUT_PLUGIN_TYPE.get(t, t).toString()); + } + List reasons = new ArrayList<>(); + + boolean acceptable = new ReferentialIntegrityPlugin().isConfigurationAcceptable( + InitializationUtils.getConfiguration(ReferentialIntegrityPluginCfgDefn.getInstance(), e), reasons); + + assertFalse(acceptable); + List actual = new ArrayList<>(); + for (LocalizableMessage reason : reasons) + { + actual.add(reason.toString()); + } + assertEquals(actual, expected); + } + + /** + * Issue #1118: a configuration stored before the check existed is still loaded when the server starts, with a + * warning for each missing type, so that the delete and modify DN clean-up keeps running. + */ + @Test(dataProvider = "checkReferencesWithoutPreOperationTypes") + public void testCheckReferencesWithoutPreOperationTypesIsLoadedWithWarning(Entry e, PluginType[] missing) + throws Exception + { + TestCaseUtils.ERROR_TEXT_WRITER.clear(); + + ReferentialIntegrityPlugin plugin = initializePlugin(e); + plugin.finalizePlugin(); + + List logged = TestCaseUtils.ERROR_TEXT_WRITER.getMessages(); + for (PluginType t : missing) + { + LocalizableMessage warning = WARN_PLUGIN_REFERENT_CHECK_REFERENCES_WITHOUT_PLUGIN_TYPE.get(e.getName(), t, t); + String record = "msgID=" + warning.ordinal() + " msg=" + warning; + assertTrue(logged.stream().anyMatch(line -> line.contains(record)), logged.toString()); + } + } + + /** + * Issue #1118: on a disabled plugin {@code check-references} can be set without the pre-operation types, but the + * plugin cannot then be enabled. + */ + @Test + public void testEnablingCheckReferencesWithoutPreOperationTypesIsRejected() throws Exception + { + try + { + assertEquals(replaceAttrEntry(configDN, "ds-cfg-enabled", "false").getResultCode(), ResultCode.SUCCESS); + assertEquals(replaceAttrEntry(configDN, dsConfigPluginType, + "postoperationdelete", + "postoperationmodifydn", + "subordinatemodifydn", + "subordinatedelete").getResultCode(), ResultCode.SUCCESS); + assertEquals(replaceAttrEntry(configDN, dsConfigEnforceIntegrity, "true").getResultCode(), ResultCode.SUCCESS); + + ModifyOperation op = replaceAttrEntry(configDN, "ds-cfg-enabled", "true"); + assertNotEquals(op.getResultCode(), ResultCode.SUCCESS); + String reason = op.getErrorMessage().toString(); + assertTrue(reason.contains("preoperationadd") && reason.contains("preoperationmodify"), reason); + } + finally + { + deleteAttrsEntry(configDN, dsConfigEnforceIntegrity); + replaceAttrEntry(configDN, dsConfigPluginType, + "postoperationdelete", + "postoperationmodifydn", + "subordinatemodifydn", + "subordinatedelete", + "preoperationadd", + "preoperationmodify"); + assertEquals(replaceAttrEntry(configDN, "ds-cfg-enabled", "true").getResultCode(), ResultCode.SUCCESS); + } + } + + /** + * Issue #1118: the entry a fresh install ships must be able to check references once + * {@code check-references} is turned on, so it has to list the pre-operation plugin types. + */ + @Test + public void testShippedEntryAcceptsCheckReferences() throws Exception + { + List ldif = shippedEntryLines(); + ldif.add("ds-cfg-check-references: true"); + + ReferentialIntegrityPlugin plugin = initializePlugin(TestCaseUtils.makeEntry(ldif.toArray(new String[0]))); + plugin.finalizePlugin(); + } + + /** + * Issue #1118: turning {@code check-references} on for a running plugin whose plugin types + * lack the pre-operation types must be refused rather than stored and silently ignored. + */ + @Test + public void testCheckReferencesWithoutPreOperationTypesIsRejected() throws Exception + { + assertEquals(replaceAttrEntry(configDN, dsConfigPluginType, + "postoperationdelete", + "postoperationmodifydn", + "subordinatemodifydn", + "subordinatedelete").getResultCode(), ResultCode.SUCCESS); + assertEquals(replaceAttrEntry(configDN, "ds-cfg-enabled", "true").getResultCode(), ResultCode.SUCCESS); + try + { + ModifyOperation op = replaceAttrEntry(configDN, dsConfigEnforceIntegrity, "true"); + assertNotEquals(op.getResultCode(), ResultCode.SUCCESS); + String reason = op.getErrorMessage().toString(); + assertTrue(reason.contains("preoperationadd") && reason.contains("preoperationmodify"), reason); + } + finally + { + replaceAttrEntry(configDN, dsConfigPluginType, + "postoperationdelete", + "postoperationmodifydn", + "subordinatemodifydn", + "subordinatedelete", + "preoperationadd", + "preoperationmodify"); + } + } + + /** The lines of the Referential Integrity plugin entry in the fresh-install config.ldif template. */ + private List shippedEntryLines() throws Exception + { + File template = new File(TestCaseUtils.getBuildRoot(), "resource/config/config.ldif"); + List entry = new ArrayList<>(); + for (String line : Files.readAllLines(template.toPath(), StandardCharsets.UTF_8)) + { + if (entry.isEmpty() && !line.equalsIgnoreCase("dn: cn=Referential Integrity,cn=Plugins,cn=config")) + { + continue; + } + if (line.isEmpty()) + { + break; + } + entry.add(line); + } + assertFalse(entry.isEmpty(), "config.ldif template no longer ships the Referential Integrity plugin"); + return entry; + } + private ReferentialIntegrityPlugin initializePlugin(Entry e) throws ConfigException, InitializationException { return InitializationUtils.initializePlugin( new ReferentialIntegrityPlugin(), e, ReferentialIntegrityPluginCfgDefn.getInstance()); diff --git a/opendj-server-legacy/src/test/java/org/opends/server/tools/upgrade/UpgradeUtilsTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/tools/upgrade/UpgradeUtilsTestCase.java index fb9eed1477..446a4efb23 100644 --- a/opendj-server-legacy/src/test/java/org/opends/server/tools/upgrade/UpgradeUtilsTestCase.java +++ b/opendj-server-legacy/src/test/java/org/opends/server/tools/upgrade/UpgradeUtilsTestCase.java @@ -19,25 +19,33 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Locale; +import org.forgerock.opendj.ldap.ByteString; import org.forgerock.opendj.ldap.DN; import org.forgerock.opendj.ldap.Entry; +import org.forgerock.opendj.ldap.Filter; import org.forgerock.opendj.ldap.schema.Schema; import org.forgerock.opendj.ldif.LDIFEntryReader; import org.forgerock.opendj.ldif.LDIFEntryWriter; import org.opends.server.DirectoryServerTestCase; import org.opends.server.TestCaseUtils; +import org.opends.server.util.BuildVersion; import org.opends.server.util.ChangeOperationType; import org.opends.server.util.StaticUtils; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import static org.opends.messages.ToolMessages.INFO_UPGRADE_TASK_ADD_REFERENTIAL_INTEGRITY_PRE_OPERATION_PLUGIN_TYPES; import static org.testng.Assert.*; /** * Tests that the issue #851 upgrade task payloads, applied through * {@link UpgradeUtils#updateConfigFile}, add the RFC 5805 transaction extended operation handler - * entries exactly once and match the fresh-install template. + * entries exactly once and match the fresh-install template, and that the issue #1118 payload + * gives the Referential Integrity plugin the plugin types of the fresh-install template. */ @SuppressWarnings("javadoc") @Test(groups = { "precommit", "tools" }, sequential = true) @@ -46,6 +54,10 @@ public class UpgradeUtilsTestCase extends DirectoryServerTestCase /** Unknown config attributes must not fail parsing, as in the upgrade tool's own schema. */ private final Schema schema = Schema.getCoreSchema().asNonStrictSchema(); + private static final String PLUGIN_TYPE = "ds-cfg-plugin-type"; + private final DN referentialIntegrityPluginDN = + DN.valueOf("cn=Referential Integrity,cn=Plugins,cn=config", schema); + /** The config.ldif template a fresh install starts from. */ private File freshInstallTemplate() { @@ -88,11 +100,111 @@ public void testAddTransactionHandlersAppliesOnceAndIsIdempotent() throws Except } } + @DataProvider + public Object[][] referentialIntegrityPluginTypesBeforeUpgrade() + { + return new Object[][] { + // The entry shipped before issue #1118. + { new String[] { "postOperationDelete", "postOperationModifyDN", "subordinateModifyDN", + "subordinateDelete" } }, + // The same entry with one type already added by hand, in the lower case dsconfig writes. + { new String[] { "postOperationDelete", "postOperationModifyDN", "subordinateModifyDN", + "subordinateDelete", "preoperationadd" } }, + }; + } + + /** Issue #1118: the task leaves the plugin with the plugin types a fresh install ships, each once. */ + @Test(dataProvider = "referentialIntegrityPluginTypesBeforeUpgrade") + public void testAddReferentialIntegrityPluginTypesMirrorsFreshInstallTemplate(final String[] pluginTypes) + throws Exception + { + final File tempDir = TestCaseUtils.createTemporaryDirectory("upgradeTask1118"); + try + { + final File config = new File(tempDir, "config.ldif"); + writeConfigWithReferentialIntegrityPluginTypes(config, pluginTypes); + + assertEquals(applyAddReferentialIntegrityPluginTypes(config), 1); + assertEquals(applyAddReferentialIntegrityPluginTypes(config), 1); + + assertEquals(referentialIntegrityPluginTypes(config), + referentialIntegrityPluginTypes(freshInstallTemplate())); + } + finally + { + StaticUtils.recursiveDelete(tempDir); + } + } + + /** Issue #1118: an upgrade from 5.1.x to 5.2.0 runs the task that adds the plugin types. */ + @Test + public void testAddReferentialIntegrityPluginTypesTaskRunsOnUpgradeTo520() throws Exception + { + final List summaries = new ArrayList<>(); + for (final UpgradeTask task : Upgrade.getUpgradeTasks(BuildVersion.valueOf("5.1.2"), BuildVersion.valueOf("5.2.0"))) + { + summaries.add(task.toString()); + } + assertTrue(summaries.contains(INFO_UPGRADE_TASK_ADD_REFERENTIAL_INTEGRITY_PRE_OPERATION_PLUGIN_TYPES.get().toString()), + summaries.toString()); + } + private int applyAdd(final File config, final String... ldifLines) throws Exception { return UpgradeUtils.updateConfigFile(config, null, ChangeOperationType.ADD, ldifLines); } + private int applyAddReferentialIntegrityPluginTypes(final File config) throws Exception + { + return UpgradeUtils.updateConfigFile(config, Filter.valueOf(Upgrade.REFERENTIAL_INTEGRITY_PLUGIN_FILTER), + ChangeOperationType.MODIFY, Upgrade.ADD_REFERENTIAL_INTEGRITY_PRE_OPERATION_PLUGIN_TYPES); + } + + private void writeConfigWithReferentialIntegrityPluginTypes(final File config, final String... pluginTypes) + throws Exception + { + int replaced = 0; + try (LDIFEntryReader reader = + new LDIFEntryReader(new FileInputStream(freshInstallTemplate())).setSchema(schema); + LDIFEntryWriter writer = new LDIFEntryWriter(new FileOutputStream(config))) + { + while (reader.hasNext()) + { + final Entry entry = reader.readEntry(); + if (entry.getName().equals(referentialIntegrityPluginDN)) + { + entry.replaceAttribute(PLUGIN_TYPE, (Object[]) pluginTypes); + replaced++; + } + writer.writeEntry(entry); + } + } + assertEquals(replaced, 1, "fresh-install template no longer ships the Referential Integrity plugin"); + } + + /** + * The plugin types of the Referential Integrity plugin, lower-cased and sorted. Duplicates are + * kept: the schema used to read matches the unknown config attribute case-exactly, so two + * values differing only in case both show. + */ + private List referentialIntegrityPluginTypes(final File config) throws Exception + { + for (final Entry entry : readEntries(config)) + { + if (entry.getName().equals(referentialIntegrityPluginDN)) + { + final List types = new ArrayList<>(); + for (final ByteString value : entry.getAttribute(PLUGIN_TYPE)) + { + types.add(value.toString().toLowerCase(Locale.ROOT)); + } + Collections.sort(types); + return types; + } + } + throw new AssertionError("no entry " + referentialIntegrityPluginDN + " in " + config); + } + private void writeConfigWithoutTransactionEntries(final File config) throws Exception { final DN startDN = dnOf(Upgrade.START_TRANSACTION_HANDLER_ENTRY);