/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2008-2009 Sun Microsystems, Inc.
 *      Portions Copyright 2011-2015 ForgeRock AS.
 *      Portions Copyrighted 2019 OGIS-RI Co., Ltd.
 */
package org.opends.server.controls;

import java.net.Socket;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;

import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.DereferenceAliasesPolicy;
import org.forgerock.opendj.ldap.ModificationType;
import org.forgerock.opendj.ldap.SearchScope;
import org.opends.server.TestCaseUtils;
import org.opends.server.protocols.ldap.*;
import org.opends.server.types.Control;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.RawAttribute;
import org.opends.server.types.RawModification;
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.TestCaseUtils.*;
import static org.opends.server.util.ServerConstants.*;
import static org.testng.Assert.*;
import org.testng.Assert;

/**
 * This class contains test cases that verify the appropriate handling of the
 * password policy control as defined in draft-behera-ldap-password-policy.
 */
public class PasswordPolicyControlTestCase
    extends ControlsTestCase
{
  /**
   * Make sure that the server is running.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @BeforeClass
  public void startServer()
         throws Exception
  {
    TestCaseUtils.startServer();
  }



  /**
   * Tests that an appropriate password policy response control is returned for
   * an add operation when the user's password is in a "must change" state.
   * This test will also ensure that the bind response is also capable of
   * including the password policy response control with the "change after
   * reset" error type set.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testAddMustChange()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);

    setPasswordPolicyProp("--set", "force-change-on-add:true");

    TestCaseUtils.addEntry(
        "dn: uid=test.user,o=test",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "uid: test.user",
        "givenName: Test",
        "sn: User",
        "cn: Test User",
        "userPassword: password",
        "ds-privilege-name: bypass-acl");

    Socket s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort());
    org.opends.server.tools.LDAPReader r = new org.opends.server.tools.LDAPReader(s);
    org.opends.server.tools.LDAPWriter w = new org.opends.server.tools.LDAPWriter(s);

    try
    {
      List<Control> controls = new ArrayList<>();
      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));

      BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(
           ByteString.valueOfUtf8("uid=test.user,o=test"), 3,
           ByteString.valueOfUtf8("password"));
      LDAPMessage message = new LDAPMessage(1, bindRequest, controls);
      w.writeMessage(message);

      message = r.readMessage();
      BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
      assertEquals(bindResponse.getResultCode(), LDAPResultCode.SUCCESS);

      controls = message.getControls();
      assertNotNull(controls);
      assertFalse(controls.isEmpty());

      assertTrue(passwordPolicyControlExists(controls, PasswordPolicyErrorType.CHANGE_AFTER_RESET));


      ArrayList<RawAttribute> rawAttrs = new ArrayList<>();
      rawAttrs.add(RawAttribute.create("objectClass", "organizationalUnit"));
      rawAttrs.add(RawAttribute.create("ou", "People"));

      AddRequestProtocolOp addRequest = new AddRequestProtocolOp(
           ByteString.valueOfUtf8("ou=People,o=test"), rawAttrs);

      controls = new ArrayList<>();
      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));

      message = new LDAPMessage(2, addRequest, controls);
      w.writeMessage(message);

      message = r.readMessage();
      AddResponseProtocolOp addResponse = message.getAddResponseProtocolOp();
      Assert.assertNotEquals(addResponse.getResultCode(), LDAPResultCode.SUCCESS);

      controls = message.getControls();
      assertNotNull(controls);
      assertFalse(controls.isEmpty());

      assertTrue(passwordPolicyControlExists(controls, PasswordPolicyErrorType.CHANGE_AFTER_RESET));
    }
    finally
    {
      setPasswordPolicyProp("--set", "force-change-on-add:false");

      StaticUtils.close(s);
    }
  }



  /**
   * Tests that an appropriate password policy response control is returned for
   * an add operation in which the proposed password is pre-encoded.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testAddPreEncodedPassword()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);

    Socket s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort());
    org.opends.server.tools.LDAPReader r = new org.opends.server.tools.LDAPReader(s);
    org.opends.server.tools.LDAPWriter w = new org.opends.server.tools.LDAPWriter(s);

    try
    {
      BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(
           ByteString.valueOfUtf8("cn=Directory Manager"), 3,
           ByteString.valueOfUtf8("password"));
      LDAPMessage message = new LDAPMessage(1, bindRequest);
      w.writeMessage(message);

      message = r.readMessage();
      BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
      assertEquals(bindResponse.getResultCode(), LDAPResultCode.SUCCESS);


      ArrayList<RawAttribute> rawAttrs = new ArrayList<>();
      rawAttrs.add(RawAttribute.create("objectClass", "inetOrgPerson"));
      rawAttrs.add(RawAttribute.create("uid", "test.user"));
      rawAttrs.add(RawAttribute.create("givenName", "Test"));
      rawAttrs.add(RawAttribute.create("sn", "User"));
      rawAttrs.add(RawAttribute.create("cn", "Test User"));
      rawAttrs.add(RawAttribute.create("userPassword",
                        "{SSHA}0pZPpMIm6xSBIW4hGvR/72fjO4M9p3Ff1g7QFw=="));

      AddRequestProtocolOp addRequest = new AddRequestProtocolOp(
           ByteString.valueOfUtf8("ou=uid=test.user,o=test"), rawAttrs);

      List<Control> controls = new ArrayList<>();
      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));

      message = new LDAPMessage(2, addRequest, controls);
      w.writeMessage(message);

      message = r.readMessage();
      AddResponseProtocolOp addResponse = message.getAddResponseProtocolOp();
      Assert.assertNotEquals(addResponse.getResultCode(), LDAPResultCode.SUCCESS);

      controls = message.getControls();
      assertNotNull(controls);
      assertFalse(controls.isEmpty());

      assertTrue(passwordPolicyControlExists(controls, PasswordPolicyErrorType.INSUFFICIENT_PASSWORD_QUALITY));
    }
    finally
    {
      StaticUtils.close(s);
    }
  }

  private boolean passwordPolicyControlExists(List<Control> controls, PasswordPolicyErrorType expectedErrorType)
      throws DirectoryException
  {
    boolean found = false;
    for(Control c : controls)
    {
      if (c.getOID().equals(OID_PASSWORD_POLICY_CONTROL))
      {
        PasswordPolicyResponseControl pwpControl;
        if(c instanceof LDAPControl)
        {
          pwpControl = PasswordPolicyResponseControl.DECODER.decode(c.isCritical(), ((LDAPControl) c).getValue());
        }
        else
        {
          pwpControl = (PasswordPolicyResponseControl)c;
        }
        assertEquals(pwpControl.getErrorType(), expectedErrorType);
        found = true;
      }
    }
    return found;
  }


  /**
   * Tests that an appropriate password policy response control is returned for
   * an add operation in which the proposed password fails validation.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testAddPasswordFailsValidation()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);

    setPasswordPolicyProp("--add", "password-validator:Length-Based Password Validator");

    Socket s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort());
    org.opends.server.tools.LDAPReader r = new org.opends.server.tools.LDAPReader(s);
    org.opends.server.tools.LDAPWriter w = new org.opends.server.tools.LDAPWriter(s);

    try
    {
      BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(
           ByteString.valueOfUtf8("cn=Directory Manager"), 3,
           ByteString.valueOfUtf8("password"));
      LDAPMessage message = new LDAPMessage(1, bindRequest);
      w.writeMessage(message);

      message = r.readMessage();
      BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
      assertEquals(bindResponse.getResultCode(), LDAPResultCode.SUCCESS);


      ArrayList<RawAttribute> rawAttrs = new ArrayList<>();
      rawAttrs.add(RawAttribute.create("objectClass", "inetOrgPerson"));
      rawAttrs.add(RawAttribute.create("uid", "test.user"));
      rawAttrs.add(RawAttribute.create("givenName", "Test"));
      rawAttrs.add(RawAttribute.create("sn", "User"));
      rawAttrs.add(RawAttribute.create("cn", "Test User"));
      rawAttrs.add(RawAttribute.create("userPassword", "short"));

      AddRequestProtocolOp addRequest = new AddRequestProtocolOp(
           ByteString.valueOfUtf8("ou=uid=test.user,o=test"), rawAttrs);

      List<Control> controls = new ArrayList<>();
      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));

      message = new LDAPMessage(2, addRequest, controls);
      w.writeMessage(message);

      message = r.readMessage();
      AddResponseProtocolOp addResponse = message.getAddResponseProtocolOp();
      Assert.assertNotEquals(addResponse.getResultCode(), LDAPResultCode.SUCCESS);

      controls = message.getControls();
      assertNotNull(controls);
      assertFalse(controls.isEmpty());

      assertTrue(passwordPolicyControlExists(controls, PasswordPolicyErrorType.INSUFFICIENT_PASSWORD_QUALITY));
    }
    finally
    {
      setPasswordPolicyProp("--remove", "password-validator:Length-Based Password Validator");

      StaticUtils.close(s);
    }
  }



  /**
   * Tests that an appropriate password policy response control is returned for
   * a bind operation in which the user's account is locked due to
   * authentication failures.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testBindLockedDueToFailures()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);

    setPasswordPolicyProp("--set", "lockout-failure-count:3");

    TestCaseUtils.addEntry(
        "dn: uid=test.user,o=test",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "uid: test.user",
        "givenName: Test",
        "sn: User",
        "cn: Test User",
        "userPassword: password",
        "ds-privilege-name: bypass-acl");

    Socket s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort());
    org.opends.server.tools.LDAPReader r = new org.opends.server.tools.LDAPReader(s);
    org.opends.server.tools.LDAPWriter w = new org.opends.server.tools.LDAPWriter(s);

    try
    {
      BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(
           ByteString.valueOfUtf8("uid=test.user,o=test"), 3,
           ByteString.valueOfUtf8("wrong"));

      for (int i=1; i <= 3; i++)
      {
        LDAPMessage message = new LDAPMessage(1, bindRequest);
        w.writeMessage(message);

        message = r.readMessage();
        BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
        Assert.assertNotEquals(bindResponse.getResultCode(), LDAPResultCode.SUCCESS);
      }

      bindRequest = new BindRequestProtocolOp(
           ByteString.valueOfUtf8("uid=test.user,o=test"), 3,
           ByteString.valueOfUtf8("password"));

      List<Control> controls = new ArrayList<>();
      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));

      LDAPMessage message = new LDAPMessage(4, bindRequest, controls);
      w.writeMessage(message);

      message = r.readMessage();
      BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
      Assert.assertNotEquals(bindResponse.getResultCode(), LDAPResultCode.SUCCESS);

      controls = message.getControls();
      assertNotNull(controls);
      assertFalse(controls.isEmpty());

      assertTrue(passwordPolicyControlExists(controls, PasswordPolicyErrorType.ACCOUNT_LOCKED));
    }
    finally
    {
      setPasswordPolicyProp("--set", "lockout-failure-count:0");

      StaticUtils.close(s);
    }
  }


  /**
   * Tests that an appropriate password policy response control is returned for
   * a compare operation when the user's password is in a "must change" state.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testCompareMustChange()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);

    setPasswordPolicyProp("--set", "force-change-on-add:true");

    TestCaseUtils.addEntry(
        "dn: uid=test.user,o=test",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "uid: test.user",
        "givenName: Test",
        "sn: User",
        "cn: Test User",
        "userPassword: password",
        "ds-privilege-name: bypass-acl");

    Socket s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort());
    org.opends.server.tools.LDAPReader r = new org.opends.server.tools.LDAPReader(s);
    org.opends.server.tools.LDAPWriter w = new org.opends.server.tools.LDAPWriter(s);

    try
    {
      BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(
           ByteString.valueOfUtf8("uid=test.user,o=test"), 3,
           ByteString.valueOfUtf8("password"));
      LDAPMessage message = new LDAPMessage(1, bindRequest);
      w.writeMessage(message);

      message = r.readMessage();
      BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
      assertEquals(bindResponse.getResultCode(), LDAPResultCode.SUCCESS);


      CompareRequestProtocolOp compareRequest =
           new CompareRequestProtocolOp(ByteString.valueOfUtf8("o=test"), "o",
                                        ByteString.valueOfUtf8("test"));

      List<Control> controls = new ArrayList<>();
      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));

      message = new LDAPMessage(2, compareRequest, controls);
      w.writeMessage(message);

      message = r.readMessage();
      CompareResponseProtocolOp compareResponse =
           message.getCompareResponseProtocolOp();
      Assert.assertNotEquals(compareResponse.getResultCode(), LDAPResultCode.SUCCESS);

      controls = message.getControls();
      assertNotNull(controls);
      assertFalse(controls.isEmpty());

      assertTrue(passwordPolicyControlExists(controls, PasswordPolicyErrorType.CHANGE_AFTER_RESET));
    }
    finally
    {
      setPasswordPolicyProp("--set", "force-change-on-add:false");

      StaticUtils.close(s);
    }
  }



  /**
   * Tests that an appropriate password policy response control is returned for
   * a delete operation when the user's password is in a "must change" state.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testDeleteMustChange()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);

    setPasswordPolicyProp("--set", "force-change-on-add:true");

    TestCaseUtils.addEntries(
        "dn: uid=test.user,o=test",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "uid: test.user",
        "givenName: Test",
        "sn: User",
        "cn: Test User",
        "userPassword: password",
        "ds-privilege-name: bypass-acl",
        "",
        "dn: ou=People,o=test",
        "objectClass: top",
        "objectClass: organizationalUnit",
        "ou: People");

    Socket s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort());
    org.opends.server.tools.LDAPReader r = new org.opends.server.tools.LDAPReader(s);
    org.opends.server.tools.LDAPWriter w = new org.opends.server.tools.LDAPWriter(s);

    try
    {
      BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(
           ByteString.valueOfUtf8("uid=test.user,o=test"), 3,
           ByteString.valueOfUtf8("password"));
      LDAPMessage message = new LDAPMessage(1, bindRequest);
      w.writeMessage(message);

      message = r.readMessage();
      BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
      assertEquals(bindResponse.getResultCode(), LDAPResultCode.SUCCESS);


      DeleteRequestProtocolOp deleteRequest =
           new DeleteRequestProtocolOp(ByteString.valueOfUtf8("ou=People,o=test"));

      List<Control> controls = new ArrayList<>();
      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));

      message = new LDAPMessage(2, deleteRequest, controls);
      w.writeMessage(message);

      message = r.readMessage();
      DeleteResponseProtocolOp deleteResponse =
           message.getDeleteResponseProtocolOp();
      Assert.assertNotEquals(deleteResponse.getResultCode(), LDAPResultCode.SUCCESS);

      controls = message.getControls();
      assertNotNull(controls);
      assertFalse(controls.isEmpty());

      assertTrue(passwordPolicyControlExists(controls, PasswordPolicyErrorType.CHANGE_AFTER_RESET));
    }
    finally
    {
      setPasswordPolicyProp("--set", "force-change-on-add:false");

      StaticUtils.close(s);
    }
  }




  /**
   * Creates test data for testModifyMustChange.
   *
   * Fields:
   *   <userDN> <entryDN> <changeAfterReset>
   *
   * @return Returns test data for testModifyMustChange.
   */
  @DataProvider(name = "testModifyMustChange")
  public Object[][] createTestModifyMustChange() {
    return new Object[][] {
        // User does not need to change their password.
        { "uid=test.admin,o=test", "uid=test.admin,o=test", false },
        { "uid=test.admin,o=test", "uid=test.user,o=test",  false },
        { "uid=test.admin,o=test", "o=test",                false },

        // User does need to change their password.
        { "uid=test.user,o=test",  "uid=test.admin,o=test", true },
        { "uid=test.user,o=test",  "uid=test.user,o=test",  true },
        { "uid=test.user,o=test",  "o=test",                true }
    };
  }



  /**
   * Tests that an appropriate password policy response control is
   * returned for a modify operation when the user's password is in a
   * "must change" state.
   *
   * @param userDN
   *          The name of the user to bind as.
   * @param entryDN
   *          The name of the entry to modify.
   * @param changeAfterReset
   *          {@code true} if change after reset is expected.
   * @throws Exception
   *           If an unexpected problem occurs.
   */
  @Test(dataProvider="testModifyMustChange")
  public void testModifyMustChange(String userDN, String entryDN, boolean changeAfterReset)
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);

    TestCaseUtils.addEntry(
        "dn: uid=test.admin,o=test",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "uid: test.admin",
        "givenName: Test Admin",
        "sn: Admin",
        "cn: Test Admin",
        "userPassword: password",
        "ds-privilege-name: bypass-acl");

    setPasswordPolicyProp("--set", "force-change-on-add:true");

    TestCaseUtils.addEntry(
        "dn: uid=test.user,o=test",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "uid: test.user",
        "givenName: Test",
        "sn: User",
        "cn: Test User",
        "userPassword: password",
        "ds-privilege-name: bypass-acl");

    Socket s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort());
    org.opends.server.tools.LDAPReader r = new org.opends.server.tools.LDAPReader(s);
    org.opends.server.tools.LDAPWriter w = new org.opends.server.tools.LDAPWriter(s);

    try
    {
      BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(
           ByteString.valueOfUtf8(userDN), 3,
           ByteString.valueOfUtf8("password"));
      LDAPMessage message = new LDAPMessage(1, bindRequest);
      w.writeMessage(message);

      message = r.readMessage();
      BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
      assertEquals(bindResponse.getResultCode(), LDAPResultCode.SUCCESS);


      ArrayList<RawModification> mods = new ArrayList<>();
      mods.add(RawModification.create(ModificationType.REPLACE, "description",
                                      "foo"));

      ModifyRequestProtocolOp modifyRequest =
           new ModifyRequestProtocolOp(ByteString.valueOfUtf8(entryDN), mods);

      List<Control> controls = new ArrayList<>();
      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));

      message = new LDAPMessage(2, modifyRequest, controls);
      w.writeMessage(message);

      message = r.readMessage();
      ModifyResponseProtocolOp modifyResponse =
           message.getModifyResponseProtocolOp();

      if (changeAfterReset)
      {
        assertEquals(modifyResponse.getResultCode(),
            LDAPResultCode.CONSTRAINT_VIOLATION);
      }
      else
      {
        assertEquals(modifyResponse.getResultCode(),
            LDAPResultCode.SUCCESS);
      }

      controls = message.getControls();
      assertNotNull(controls);
      assertFalse(controls.isEmpty());

      boolean found = false;
      for(Control c : controls)
      {
        if (c.getOID().equals(OID_PASSWORD_POLICY_CONTROL))
        {
          PasswordPolicyResponseControl pwpControl;
          if(c instanceof LDAPControl)
          {
            pwpControl =
                PasswordPolicyResponseControl.DECODER.decode(c.isCritical(), ((LDAPControl)c).getValue());
          }
          else
          {
            pwpControl = (PasswordPolicyResponseControl)c;
          }
          if (changeAfterReset) {
            assertEquals(pwpControl.getErrorType(),
                         PasswordPolicyErrorType.CHANGE_AFTER_RESET);
          } else {
            assertNull(pwpControl.getErrorType());
          }
          found = true;
        }
      }
      assertTrue(found);
    }
    finally
    {
      setPasswordPolicyProp("--set", "force-change-on-add:false");

      StaticUtils.close(s);
    }
  }



  /**
   * Tests that an appropriate password policy response control is
   * returned for a modify operation when the authorized user is forced to
   * change their own password before changing a different entry.
   *
   * @throws Exception
   *           If an unexpected problem occurs.
   */
  @Test
  public void testAuthzModifyMustChange()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);

    String userDN = "uid=test.admin,o=test";
    String entryDN = "uid=test.user,o=test";
    String authzDN = "uid=authz.user,o=test";

    TestCaseUtils.addEntry(
        "dn: uid=test.admin,o=test",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "uid: test.admin",
        "givenName: Test Admin",
        "sn: Admin",
        "cn: Test Admin",
        "userPassword: password",
        "ds-privilege-name: bypass-acl",
        "ds-privilege-name: proxied-auth");

    TestCaseUtils.addEntry(
        "dn: uid=test.user,o=test",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "uid: test.user",
        "givenName: Test",
        "sn: User",
        "cn: Test User",
        "userPassword: password",
        "ds-privilege-name: bypass-acl");

    setPasswordPolicyProp("--set", "force-change-on-add:true");

    TestCaseUtils.addEntry(
        "dn: uid=authz.user,o=test",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "uid: authz.user",
        "givenName: Authz",
        "sn: User",
        "cn: Authz User",
        "userPassword: password",
        "ds-privilege-name: bypass-acl");

    Socket s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort());
    org.opends.server.tools.LDAPReader r = new org.opends.server.tools.LDAPReader(s);
    org.opends.server.tools.LDAPWriter w = new org.opends.server.tools.LDAPWriter(s);

    try
    {
      BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(
           ByteString.valueOfUtf8(userDN), 3,
           ByteString.valueOfUtf8("password"));
      LDAPMessage message = new LDAPMessage(1, bindRequest);
      w.writeMessage(message);

      message = r.readMessage();
      BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
      assertEquals(bindResponse.getResultCode(), LDAPResultCode.SUCCESS);


      ArrayList<RawModification> mods = new ArrayList<>();
      mods.add(RawModification.create(ModificationType.REPLACE, "description",
                                      "foo"));

      ModifyRequestProtocolOp modifyRequest =
           new ModifyRequestProtocolOp(ByteString.valueOfUtf8(entryDN), mods);

      List<Control> controls = new ArrayList<>();
      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));
      controls.add(new LDAPControl(OID_PROXIED_AUTH_V2, true,
          ByteString.valueOfUtf8("dn:" + authzDN)));

      message = new LDAPMessage(2, modifyRequest, controls);
      w.writeMessage(message);

      message = r.readMessage();
      ModifyResponseProtocolOp modifyResponse =
           message.getModifyResponseProtocolOp();

      assertEquals(modifyResponse.getResultCode(),
          LDAPResultCode.CONSTRAINT_VIOLATION);

      controls = message.getControls();
      assertNotNull(controls);
      assertFalse(controls.isEmpty());

      assertTrue(passwordPolicyControlExists(controls, PasswordPolicyErrorType.CHANGE_AFTER_RESET));
    }
    finally
    {
      setPasswordPolicyProp("--set", "force-change-on-add:false");

      StaticUtils.close(s);
    }
  }



  /**
   * Tests that an appropriate password policy response control is returned for
   * a modify operation when users do not have permission to change their own
   * passwords.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testModifyCannotChange()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);

    setPasswordPolicyProp("--set", "allow-user-password-changes:false");

    TestCaseUtils.addEntry(
        "dn: uid=test.user,o=test",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "uid: test.user",
        "givenName: Test",
        "sn: User",
        "cn: Test User",
        "userPassword: password",
        "ds-privilege-name: bypass-acl");

    Socket s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort());
    org.opends.server.tools.LDAPReader r = new org.opends.server.tools.LDAPReader(s);
    org.opends.server.tools.LDAPWriter w = new org.opends.server.tools.LDAPWriter(s);

    try
    {
      BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(
           ByteString.valueOfUtf8("uid=test.user,o=test"), 3,
           ByteString.valueOfUtf8("password"));
      LDAPMessage message = new LDAPMessage(1, bindRequest);
      w.writeMessage(message);

      message = r.readMessage();
      BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
      assertEquals(bindResponse.getResultCode(), LDAPResultCode.SUCCESS);


      ArrayList<RawModification> mods = new ArrayList<>();
      mods.add(RawModification.create(ModificationType.REPLACE, "userPassword",
                                      "newpassword"));

      ModifyRequestProtocolOp modifyRequest =
           new ModifyRequestProtocolOp(
                    ByteString.valueOfUtf8("uid=test.user,o=test"), mods);

      List<Control> controls = new ArrayList<>();
      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));

      message = new LDAPMessage(2, modifyRequest, controls);
      w.writeMessage(message);

      message = r.readMessage();
      ModifyResponseProtocolOp modifyResponse =
           message.getModifyResponseProtocolOp();
      Assert.assertNotEquals(modifyResponse.getResultCode(), LDAPResultCode.SUCCESS);

      controls = message.getControls();
      assertNotNull(controls);
      assertFalse(controls.isEmpty());

      assertTrue(passwordPolicyControlExists(controls, PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED));
    }
    finally
    {
      setPasswordPolicyProp("--set", "allow-user-password-changes:true");

      StaticUtils.close(s);
    }
  }

  /**
   * Tests that an appropriate password policy response control is returned for
   * a modify operation when the proposed password is in the user's password
   * history.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testModifyPasswordInHistory()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);

    setPasswordPolicyProp("--set", "password-history-count:5");

    TestCaseUtils.addEntry(
        "dn: uid=test.user,o=test",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "uid: test.user",
        "givenName: Test",
        "sn: User",
        "cn: Test User",
        "userPassword: password",
        "ds-privilege-name: bypass-acl");

    Socket s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort());
    org.opends.server.tools.LDAPReader r = new org.opends.server.tools.LDAPReader(s);
    org.opends.server.tools.LDAPWriter w = new org.opends.server.tools.LDAPWriter(s);

    try
    {
      BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(
           ByteString.valueOfUtf8("uid=test.user,o=test"), 3,
           ByteString.valueOfUtf8("password"));
      LDAPMessage message = new LDAPMessage(1, bindRequest);
      w.writeMessage(message);

      message = r.readMessage();
      BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
      assertEquals(bindResponse.getResultCode(), LDAPResultCode.SUCCESS);


      ArrayList<RawModification> mods = new ArrayList<>();
      mods.add(RawModification.create(ModificationType.REPLACE, "userPassword",
                                      "password"));

      ModifyRequestProtocolOp modifyRequest =
           new ModifyRequestProtocolOp(
                    ByteString.valueOfUtf8("uid=test.user,o=test"), mods);

      List<Control> controls = new ArrayList<>();
      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));

      message = new LDAPMessage(2, modifyRequest, controls);
      w.writeMessage(message);

      message = r.readMessage();
      ModifyResponseProtocolOp modifyResponse =
           message.getModifyResponseProtocolOp();
      Assert.assertNotEquals(modifyResponse.getResultCode(), LDAPResultCode.SUCCESS);

      controls = message.getControls();
      assertNotNull(controls);
      assertFalse(controls.isEmpty());

      assertTrue(passwordPolicyControlExists(controls, PasswordPolicyErrorType.PASSWORD_IN_HISTORY));
    }
    finally
    {
      setPasswordPolicyProp("--set", "password-history-count:0");

      StaticUtils.close(s);
    }
  }



  /**
   * Tests that an appropriate password policy response control is returned for
   * a modify operation when the user didn't provide their current password when
   * it was required.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testModifyMissingCurrentPassword()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);

    setPasswordPolicyProp("--set", "password-change-requires-current-password:true");

    TestCaseUtils.addEntry(
        "dn: uid=test.user,o=test",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "uid: test.user",
        "givenName: Test",
        "sn: User",
        "cn: Test User",
        "userPassword: password",
        "ds-privilege-name: bypass-acl");

    Socket s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort());
    org.opends.server.tools.LDAPReader r = new org.opends.server.tools.LDAPReader(s);
    org.opends.server.tools.LDAPWriter w = new org.opends.server.tools.LDAPWriter(s);

    try
    {
      BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(
           ByteString.valueOfUtf8("uid=test.user,o=test"), 3,
           ByteString.valueOfUtf8("password"));
      LDAPMessage message = new LDAPMessage(1, bindRequest);
      w.writeMessage(message);

      message = r.readMessage();
      BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
      assertEquals(bindResponse.getResultCode(), LDAPResultCode.SUCCESS);


      ArrayList<RawModification> mods = new ArrayList<>();
      mods.add(RawModification.create(ModificationType.REPLACE, "userPassword",
                                      "newpassword"));

      ModifyRequestProtocolOp modifyRequest =
           new ModifyRequestProtocolOp(
                    ByteString.valueOfUtf8("uid=test.user,o=test"), mods);

      List<Control> controls = new ArrayList<>();
      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));

      message = new LDAPMessage(2, modifyRequest, controls);
      w.writeMessage(message);

      message = r.readMessage();
      ModifyResponseProtocolOp modifyResponse =
           message.getModifyResponseProtocolOp();
      Assert.assertNotEquals(modifyResponse.getResultCode(), LDAPResultCode.SUCCESS);

      controls = message.getControls();
      assertNotNull(controls);
      assertFalse(controls.isEmpty());

      assertTrue(passwordPolicyControlExists(controls, PasswordPolicyErrorType.MUST_SUPPLY_OLD_PASSWORD));
    }
    finally
    {
      setPasswordPolicyProp("--set", "password-change-requires-current-password:false");

      StaticUtils.close(s);
    }
  }



  /**
   * Tests that an appropriate password policy response control is returned for
   * a modify operation when the user tried to perform multiple password changes
   * without respecting the minimum age.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testModifyMinimumPasswordAge()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);

    setPasswordPolicyProp("--set", "min-password-age:24 hours");

    TestCaseUtils.addEntry(
        "dn: uid=test.user,o=test",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "uid: test.user",
        "givenName: Test",
        "sn: User",
        "cn: Test User",
        "userPassword: password",
        "ds-privilege-name: bypass-acl");

    Socket s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort());
    org.opends.server.tools.LDAPReader r = new org.opends.server.tools.LDAPReader(s);
    org.opends.server.tools.LDAPWriter w = new org.opends.server.tools.LDAPWriter(s);

    try
    {
      BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(
           ByteString.valueOfUtf8("uid=test.user,o=test"), 3,
           ByteString.valueOfUtf8("password"));
      LDAPMessage message = new LDAPMessage(1, bindRequest);
      w.writeMessage(message);

      message = r.readMessage();
      BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
      assertEquals(bindResponse.getResultCode(), LDAPResultCode.SUCCESS);


      ArrayList<RawModification> mods = new ArrayList<>();
      mods.add(RawModification.create(ModificationType.REPLACE, "userPassword",
                                      "newpassword"));

      ModifyRequestProtocolOp modifyRequest =
           new ModifyRequestProtocolOp(
                    ByteString.valueOfUtf8("uid=test.user,o=test"), mods);

      List<Control> controls = new ArrayList<>();
      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));

      message = new LDAPMessage(2, modifyRequest, controls);
      w.writeMessage(message);

      message = r.readMessage();
      ModifyResponseProtocolOp modifyResponse =
           message.getModifyResponseProtocolOp();
      Assert.assertNotEquals(modifyResponse.getResultCode(), LDAPResultCode.SUCCESS);

      controls = message.getControls();
      assertNotNull(controls);
      assertFalse(controls.isEmpty());

      assertTrue(passwordPolicyControlExists(controls, PasswordPolicyErrorType.PASSWORD_TOO_YOUNG));
    }
    finally
    {
      setPasswordPolicyProp("--set", "min-password-age:0 seconds");

      StaticUtils.close(s);
    }
  }



  /**
   * Tests that an appropriate password policy response control is returned for
   * a modify DN operation when the user's password is in a "must change" state.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testModifyDNMustChange()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);

    setPasswordPolicyProp("--set", "force-change-on-add:true");

    TestCaseUtils.addEntries(
        "dn: uid=test.user,o=test",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "uid: test.user",
        "givenName: Test",
        "sn: User",
        "cn: Test User",
        "userPassword: password",
        "ds-privilege-name: bypass-acl",
        "",
        "dn: ou=People,o=test",
        "objectClass: top",
        "objectClass: organizationalUnit",
        "ou: People");

    Socket s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort());
    org.opends.server.tools.LDAPReader r = new org.opends.server.tools.LDAPReader(s);
    org.opends.server.tools.LDAPWriter w = new org.opends.server.tools.LDAPWriter(s);

    try
    {
      BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(
           ByteString.valueOfUtf8("uid=test.user,o=test"), 3,
           ByteString.valueOfUtf8("password"));
      LDAPMessage message = new LDAPMessage(1, bindRequest);
      w.writeMessage(message);

      message = r.readMessage();
      BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
      assertEquals(bindResponse.getResultCode(), LDAPResultCode.SUCCESS);


      ModifyDNRequestProtocolOp modifyDNRequest =
           new ModifyDNRequestProtocolOp(
                    ByteString.valueOfUtf8("ou=People,o=test"),
                    ByteString.valueOfUtf8("ou=Users"), true);

      List<Control> controls = new ArrayList<>();
      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));

      message = new LDAPMessage(2, modifyDNRequest, controls);
      w.writeMessage(message);

      message = r.readMessage();
      ModifyDNResponseProtocolOp modifyDNResponse =
           message.getModifyDNResponseProtocolOp();
      Assert.assertNotEquals(modifyDNResponse.getResultCode(), LDAPResultCode.SUCCESS);

      controls = message.getControls();
      assertNotNull(controls);
      assertFalse(controls.isEmpty());

      assertTrue(passwordPolicyControlExists(controls, PasswordPolicyErrorType.CHANGE_AFTER_RESET));
    }
    finally
    {
      setPasswordPolicyProp("--set", "force-change-on-add:false");

      StaticUtils.close(s);
    }
  }

  /**
   * Tests that an appropriate password policy response control is returned for
   * a search operation when the user's password is in a "must change" state.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test
  public void testSearchMustChange()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);

    setPasswordPolicyProp("--set", "force-change-on-add:true");

    TestCaseUtils.addEntry(
        "dn: uid=test.user,o=test",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "uid: test.user",
        "givenName: Test",
        "sn: User",
        "cn: Test User",
        "userPassword: password",
        "ds-privilege-name: bypass-acl");

    Socket s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort());
    org.opends.server.tools.LDAPReader r = new org.opends.server.tools.LDAPReader(s);
    org.opends.server.tools.LDAPWriter w = new org.opends.server.tools.LDAPWriter(s);

    try
    {
      BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(
           ByteString.valueOfUtf8("uid=test.user,o=test"), 3,
           ByteString.valueOfUtf8("password"));
      LDAPMessage message = new LDAPMessage(1, bindRequest);
      w.writeMessage(message);

      message = r.readMessage();
      BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
      assertEquals(bindResponse.getResultCode(), LDAPResultCode.SUCCESS);


      SearchRequestProtocolOp searchRequest =
           new SearchRequestProtocolOp(ByteString.valueOfUtf8("o=test"),
                                       SearchScope.BASE_OBJECT,
                                       DereferenceAliasesPolicy.NEVER, 0, 0, false,
                                       LDAPFilter.objectClassPresent(),
                                       new LinkedHashSet<String>());

      List<Control> controls = new ArrayList<>();
      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));

      message = new LDAPMessage(2, searchRequest, controls);
      w.writeMessage(message);

      message = r.readMessage();
      SearchResultDoneProtocolOp searchDone =
           message.getSearchResultDoneProtocolOp();
      Assert.assertNotEquals(searchDone.getResultCode(), LDAPResultCode.SUCCESS);

      controls = message.getControls();
      assertNotNull(controls);
      assertFalse(controls.isEmpty());

      assertTrue(passwordPolicyControlExists(controls, PasswordPolicyErrorType.CHANGE_AFTER_RESET));
    }
    finally
    {
      setPasswordPolicyProp("--set", "force-change-on-add:false");

      StaticUtils.close(s);
    }
  }

  private void setPasswordPolicyProp(String arg, String value)
  {
    TestCaseUtils.dsconfig("set-password-policy-prop", "--policy-name", "Default Password Policy", arg, value);
  }
}
