mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valery Kharseko
15 hours ago dd94891ad6946da5646fffdf11631144725b5636
opendj-server-legacy/src/test/java/org/openidentityplatform/opendj/RelaxRulesTestCase.java
@@ -11,14 +11,18 @@
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2024 3A Systems, LLC.
 * Copyright 2024-2026 3A Systems, LLC.
 */
package org.openidentityplatform.opendj;
import org.forgerock.opendj.adapter.server3x.Adapters;
import org.forgerock.opendj.ldap.*;
import org.forgerock.opendj.ldap.controls.Control;
import org.forgerock.opendj.ldap.controls.GenericControl;
import org.forgerock.opendj.ldap.controls.ProxiedAuthV2RequestControl;
import org.forgerock.opendj.ldap.controls.RelaxRulesControl;
import org.forgerock.opendj.ldap.requests.AddRequest;
import org.forgerock.opendj.ldap.requests.ModifyRequest;
import org.forgerock.opendj.ldap.requests.Requests;
import org.forgerock.opendj.ldap.responses.Result;
@@ -26,6 +30,7 @@
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
@@ -33,7 +38,22 @@
@Test(sequential = true)
public class RelaxRulesTestCase extends DirectoryServerTestCase {
    /** A client which may write anything under the suffix, but has no {@code bypass-acl} privilege. */
    private static final String USER_DN = "uid=relax.user,o=test";
    /** A client with the {@code bypass-acl} privilege which may act as {@link #USER_DN}. */
    private static final String PROXY_DN = "uid=relax.proxy,o=test";
    private static final String ACCESS_HANDLER_DN = "cn=Access Control Handler,cn=config";
    /** Lets any authenticated client send the Relax Rules control, so that it survives the ACI of the controls. */
    private static final String RELAX_CONTROL_ACI = "(targetcontrol=\"" + RelaxRulesControl.OID + "\")"
            + "(version 3.0; acl \"Relax Rules control access\"; allow(read) userdn=\"ldap:///all\";)";
    /** The global ACI of the test configuration which lets anyone use any control. */
    private static final String ANY_CONTROL_ACI = "(targetcontrol=\"*\")"
            + " (version 3.0; acl \"Anonymous control access\"; allow(read) userdn=\"ldap:///anyone\";)";
    private static final String OLD_TIME = "20211203224637.000Z";
    private static final String PRE_ENCODED_PASSWORD = "{SSHA}K9Hv0w7Z0Q2yL3ZJmD1m4n8q3mYk1Xn1R0x6Xw==";
    Connection connection;
    private LDAPConnectionFactory factory;
    @BeforeClass
    public void startServer() throws Exception {
@@ -53,13 +73,50 @@
            "userPassword:: cGFzc3dvcmQ=",
            "postalAddress: Aarika Atpco$00900 Maple Street$New Orleans, KS  10857",
            "postalCode: 10857",
            ""
            "",
            "dn: " + USER_DN,
            "objectClass: top",
            "objectClass: person",
            "objectClass: inetOrgPerson",
            "objectClass: organizationalPerson",
            "cn: Relax User",
            "sn: User",
            "uid: relax.user",
            "userPassword: password",
            "",
            "dn: " + PROXY_DN,
            "objectClass: top",
            "objectClass: person",
            "objectClass: inetOrgPerson",
            "objectClass: organizationalPerson",
            "cn: Relax Proxy",
            "sn: Proxy",
            "uid: relax.proxy",
            "userPassword: password",
            "ds-privilege-name: bypass-acl",
            "ds-privilege-name: proxied-auth"
        );
        final LDAPConnectionFactory factory =new LDAPConnectionFactory("localhost", TestCaseUtils.getServerLdapPort());
        factory = new LDAPConnectionFactory("localhost", TestCaseUtils.getServerLdapPort());
        connection = factory.getConnection();
        connection.bind("cn=Directory Manager", "password".toCharArray());
        assertThat(connection.isValid()).isTrue();
        // The user may write anything under the suffix, operational attributes included: what stops it
        // below is the Relax Rules control alone.
        connection.modify(Requests.newModifyRequest("o=test").addModification(ModificationType.ADD, "aci",
                "(targetattr=\"*||+\")(version 3.0; acl \"Relax Rules test user\"; allow(all) userdn=\"ldap:///"
                        + USER_DN + "\";)"));
    }
    @AfterClass(alwaysRun = true)
    public void closeConnection() {
        if (connection != null) {
            connection.close();
        }
        if (factory != null) {
            factory.close();
        }
    }
    @Test
@@ -80,4 +137,280 @@
        assertThat(srEntry.getAttribute("pwdChangedTime").firstValueAsString()).isEqualTo(
                "20211203224637.000Z");
    }
    @Test
    public void aRelaxedModifyStillRunsThePreOperationPlugins() throws Exception {
        final String dn = addPerson("plugins");
        connection.modify(Requests.newModifyRequest(dn)
                .addControl(new RelaxRulesControl())
                .addModification(ModificationType.REPLACE, "description", "relaxed"));
        assertThat(valueOf(dn, "modifiersName")).as("the last modified plugin did not run")
                .isEqualToIgnoringCase("cn=Directory Manager,cn=Root DNs,cn=config");
    }
    @Test
    public void aRelaxedModifyKeepsTheLastModifiedValuesTheClientSupplies() throws Exception {
        final String dn = addPerson("lastmod modify");
        connection.modify(Requests.newModifyRequest(dn)
                .addControl(new RelaxRulesControl())
                .addModification(ModificationType.REPLACE, "modifyTimestamp", OLD_TIME)
                .addModification(ModificationType.REPLACE, "modifiersName", "cn=migrated"));
        assertThat(valueOf(dn, "modifyTimestamp")).isEqualTo(OLD_TIME);
        assertThat(valueOf(dn, "modifiersName")).isEqualTo("cn=migrated");
    }
    @Test
    public void aRelaxedAddKeepsTheCreationValuesTheClientSupplies() throws Exception {
        final String dn = "cn=lastmod add,o=test";
        connection.add(person(dn)
                .addAttribute("createTimestamp", OLD_TIME)
                .addAttribute("creatorsName", "cn=migrated")
                .addControl(new RelaxRulesControl()));
        assertThat(valueOf(dn, "createTimestamp")).isEqualTo(OLD_TIME);
        assertThat(valueOf(dn, "creatorsName")).isEqualTo("cn=migrated");
    }
    @Test
    public void aRelaxedModifyKeepsAMigratedPasswordAndItsChangeTime() throws Exception {
        final String dn = addPerson("password modify");
        connection.modify(Requests.newModifyRequest(dn)
                .addControl(new RelaxRulesControl())
                .addModification(ModificationType.REPLACE, "userPassword", PRE_ENCODED_PASSWORD)
                .addModification(ModificationType.REPLACE, "pwdChangedTime", OLD_TIME));
        assertThat(valueOf(dn, "userPassword")).isEqualTo(PRE_ENCODED_PASSWORD);
        assertThat(valueOf(dn, "pwdChangedTime")).isEqualTo(OLD_TIME);
    }
    @Test
    public void aRelaxedAddKeepsAMigratedPasswordAndItsChangeTime() throws Exception {
        final String dn = "cn=password add,o=test";
        connection.add(person(dn)
                .addAttribute("userPassword", PRE_ENCODED_PASSWORD)
                .addAttribute("pwdChangedTime", OLD_TIME)
                .addControl(new RelaxRulesControl()));
        assertThat(valueOf(dn, "userPassword")).isEqualTo(PRE_ENCODED_PASSWORD);
        assertThat(valueOf(dn, "pwdChangedTime")).isEqualTo(OLD_TIME);
    }
    @Test
    public void aRelaxedAddSkipsTheSchemaCheck() throws Exception {
        // person does not allow mail.
        assertThat(resultOf(() -> connection.add(person("cn=schema add,o=test").addAttribute("mail", "x@example.com"))))
                .isEqualTo(ResultCode.OBJECTCLASS_VIOLATION);
        connection.add(person("cn=schema add,o=test")
                .addAttribute("mail", "x@example.com")
                .addControl(new RelaxRulesControl()));
        assertThat(valueOf("cn=schema add,o=test", "mail")).isEqualTo("x@example.com");
    }
    @Test
    public void aRelaxedModifySkipsTheSchemaCheck() throws Exception {
        final String dn = addPerson("schema modify");
        final ModifyRequest addMail = Requests.newModifyRequest(dn)
                .addModification(ModificationType.ADD, "mail", "x@example.com");
        assertThat(resultOf(() -> connection.modify(addMail))).isEqualTo(ResultCode.OBJECTCLASS_VIOLATION);
        connection.modify(Requests.copyOfModifyRequest(addMail).addControl(new RelaxRulesControl()));
        assertThat(valueOf(dn, "mail")).isEqualTo("x@example.com");
    }
    @Test
    public void aRelaxedModifyMayWriteAnObsoleteAttribute() throws Exception {
        final String obsoleteType = "( 1.3.6.1.4.1.36733.2.1.999.1053 NAME 'relaxRulesObsoleteTest' OBSOLETE"
                + " SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'RelaxRulesTestCase' )";
        connection.modify(Requests.newModifyRequest("cn=schema")
                .addModification(ModificationType.ADD, "attributeTypes", obsoleteType));
        try {
            final String dn = "cn=obsolete,o=test";
            connection.add(Requests.newAddRequest(dn)
                    .addAttribute("objectClass", "top", "person", "extensibleObject")
                    .addAttribute("cn", "obsolete")
                    .addAttribute("sn", "obsolete"));
            final ModifyRequest writeObsolete = Requests.newModifyRequest(dn)
                    .addModification(ModificationType.REPLACE, "relaxRulesObsoleteTest", "value");
            assertThat(resultOf(() -> connection.modify(writeObsolete))).isEqualTo(ResultCode.CONSTRAINT_VIOLATION);
            connection.modify(Requests.copyOfModifyRequest(writeObsolete).addControl(new RelaxRulesControl()));
            assertThat(valueOf(dn, "relaxRulesObsoleteTest")).isEqualTo("value");
            connection.delete(dn);
        } finally {
            connection.modify(Requests.newModifyRequest("cn=schema")
                    .addModification(ModificationType.DELETE, "attributeTypes", obsoleteType));
        }
    }
    /**
     * A non-critical control the client may not use is dropped (RFC 4511 4.1.11): the request is then an
     * ordinary one, and succeeds as such.
     */
    @Test
    public void aNonCriticalControlTheClientMayNotUseIsIgnored() throws Exception {
        final String dn = addPerson("ignored control");
        try (ControlAccess restricted = restrictControlAccess(null);
             Connection user = bindAsUser()) {
            user.modify(Requests.newModifyRequest(dn)
                    .addControl(nonCriticalRelaxRules())
                    .addModification(ModificationType.REPLACE, "description", "ordinary"));
            user.add(person("cn=ignored control add,o=test").addControl(nonCriticalRelaxRules()));
        }
        assertThat(valueOf(dn, "description")).isEqualTo("ordinary");
        assertThat(exists("cn=ignored control add,o=test")).isTrue();
    }
    /** Once the control ACI lets the control through, only the {@code bypass-acl} privilege lets it relax anything. */
    @Test
    public void aClientWithoutBypassAclMayNotRelaxTheRules() throws Exception {
        final String dn = addPerson("unprivileged");
        try (ControlAccess relaxAllowed = restrictControlAccess(RELAX_CONTROL_ACI);
             Connection user = bindAsUser()) {
            assertThat(resultOf(() -> user.add(person("cn=unprivileged add,o=test")
                    .addAttribute("pwdChangedTime", OLD_TIME)
                    .addControl(new RelaxRulesControl()))))
                    .isNotEqualTo(ResultCode.SUCCESS);
            assertThat(resultOf(() -> user.add(person("cn=unprivileged plain add,o=test")
                    .addControl(new RelaxRulesControl()))))
                    .isEqualTo(ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
            assertThat(resultOf(() -> user.modify(Requests.newModifyRequest(dn)
                    .addControl(new RelaxRulesControl())
                    .addModification(ModificationType.REPLACE, "pwdChangedTime", OLD_TIME))))
                    .isEqualTo(ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
        }
        assertThat(exists("cn=unprivileged add,o=test")).isFalse();
        assertThat(exists("cn=unprivileged plain add,o=test")).isFalse();
        assertThat(valueOf(dn, "pwdChangedTime")).isNull();
    }
    /**
     * The privilege which lets the rules be relaxed is the one of the identity the request runs as: a
     * client with {@code bypass-acl} which proxies as an ordinary user relaxes nothing, whether the control
     * ACI lets the control through for that user or not.
     */
    @Test
    public void aProxiedIdentityWithoutBypassAclMayNotRelaxTheRules() throws Exception {
        assertAProxiedRelaxedChangeIsRefused("proxied kept");
        try (ControlAccess restricted = restrictControlAccess(null)) {
            assertAProxiedRelaxedChangeIsRefused("proxied dropped");
        }
    }
    private void assertAProxiedRelaxedChangeIsRefused(String cn) throws Exception {
        final String dn = addPerson(cn);
        final String addedDN = "cn=" + cn + " add,o=test";
        final ProxiedAuthV2RequestControl asUser = ProxiedAuthV2RequestControl.newControl("dn:" + USER_DN);
        try (Connection proxy = factory.getConnection()) {
            proxy.bind(PROXY_DN, "password".toCharArray());
            // Without the Relax Rules control, the proxy may act as the user.
            proxy.modify(Requests.newModifyRequest(dn)
                    .addControl(asUser)
                    .addModification(ModificationType.REPLACE, "description", "proxied"));
            assertThat(resultOf(() -> proxy.add(person(addedDN)
                    .addAttribute("pwdChangedTime", OLD_TIME)
                    .addControl(asUser)
                    .addControl(nonCriticalRelaxRules()))))
                    .isNotEqualTo(ResultCode.SUCCESS);
            assertThat(resultOf(() -> proxy.modify(Requests.newModifyRequest(dn)
                    .addControl(asUser)
                    .addControl(nonCriticalRelaxRules())
                    .addModification(ModificationType.REPLACE, "pwdChangedTime", OLD_TIME))))
                    .isNotEqualTo(ResultCode.SUCCESS);
        }
        assertThat(valueOf(dn, "modifiersName")).isEqualToIgnoringCase(USER_DN);
        assertThat(exists(addedDN)).isFalse();
        assertThat(valueOf(dn, "pwdChangedTime")).isNull();
    }
    /**
     * Removes the global ACI of the test configuration which lets anyone use any control, adding the provided
     * one instead if any, until closed.
     */
    private ControlAccess restrictControlAccess(String aci) throws LdapException {
        connection.modify(Requests.newModifyRequest(ACCESS_HANDLER_DN)
                .addModification(ModificationType.DELETE, "ds-cfg-global-aci", ANY_CONTROL_ACI));
        if (aci != null) {
            connection.modify(Requests.newModifyRequest(ACCESS_HANDLER_DN)
                    .addModification(ModificationType.ADD, "ds-cfg-global-aci", aci));
        }
        return () -> {
            if (aci != null) {
                connection.modify(Requests.newModifyRequest(ACCESS_HANDLER_DN)
                        .addModification(ModificationType.DELETE, "ds-cfg-global-aci", aci));
            }
            connection.modify(Requests.newModifyRequest(ACCESS_HANDLER_DN)
                    .addModification(ModificationType.ADD, "ds-cfg-global-aci", ANY_CONTROL_ACI));
        };
    }
    private interface ControlAccess extends AutoCloseable {
        @Override
        void close() throws LdapException;
    }
    /** {@link RelaxRulesControl} is always critical. */
    private static Control nonCriticalRelaxRules() {
        return GenericControl.newControl(RelaxRulesControl.OID, false);
    }
    private String addPerson(String cn) throws LdapException {
        final String dn = "cn=" + cn + ",o=test";
        connection.add(person(dn));
        return dn;
    }
    private static AddRequest person(String dn) {
        final String cn = DN.valueOf(dn).rdn().getFirstAVA().getAttributeValue().toString();
        return Requests.newAddRequest(dn)
                .addAttribute("objectClass", "top", "person")
                .addAttribute("cn", cn)
                .addAttribute("sn", cn);
    }
    private Connection bindAsUser() throws LdapException {
        final Connection user = factory.getConnection();
        user.bind(USER_DN, "password".toCharArray());
        return user;
    }
    private String valueOf(String dn, String attribute) throws LdapException {
        final SearchResultEntry entry = connection.searchSingleEntry(
                Requests.newSearchRequest(dn, SearchScope.BASE_OBJECT, "(objectClass=*)").addAttribute("*", "+"));
        final Attribute attr = entry.getAttribute(attribute);
        return attr != null ? attr.firstValueAsString() : null;
    }
    private boolean exists(String dn) {
        return resultOf(() -> connection.searchSingleEntry(
                Requests.newSearchRequest(dn, SearchScope.BASE_OBJECT, "(objectClass=*)"))) == ResultCode.SUCCESS;
    }
    private interface Request {
        Object send() throws LdapException;
    }
    private static ResultCode resultOf(Request request) {
        try {
            request.send();
            return ResultCode.SUCCESS;
        } catch (LdapException e) {
            return e.getResult().getResultCode();
        }
    }
}