From 922296e6b25dc68bbecf1f415ba3a61aa0105b99 Mon Sep 17 00:00:00 2001
From: neil_a_wilson <neil_a_wilson@localhost>
Date: Fri, 06 Oct 2006 17:01:56 +0000
Subject: [PATCH] Update the password validator API in the following ways:

---
 opendj-sdk/opends/src/server/org/opends/server/core/PasswordPolicyState.java                                                |   22 
 opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/core/PasswordPolicyTestCase.java                     |    4 
 opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/api/PasswordValidatorTestCase.java                   |  866 ++++++++++++++++++++++++++++++++++++
 opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LengthBasedPasswordValidatorTestCase.java |   43 +
 opendj-sdk/opends/src/server/org/opends/server/extensions/PasswordModifyExtendedOperation.java                              |   22 
 opendj-sdk/opends/tests/unit-tests-testng/resource/config-changes.ldif                                                      |   16 
 opendj-sdk/opends/src/server/org/opends/server/extensions/LengthBasedPasswordValidator.java                                 |   87 --
 opendj-sdk/opends/src/server/org/opends/server/core/ModifyOperation.java                                                    |   45 +
 opendj-sdk/opends/src/server/org/opends/server/core/AddOperation.java                                                       |    6 
 opendj-sdk/opends/src/server/org/opends/server/api/PasswordValidator.java                                                   |   33 
 opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/TestPasswordValidator.java                |  237 +++++++++
 11 files changed, 1,270 insertions(+), 111 deletions(-)

diff --git a/opendj-sdk/opends/src/server/org/opends/server/api/PasswordValidator.java b/opendj-sdk/opends/src/server/org/opends/server/api/PasswordValidator.java
index d9cdfbf..195a2df 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/api/PasswordValidator.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/api/PasswordValidator.java
@@ -28,6 +28,8 @@
 
 
 
+import java.util.Set;
+
 import org.opends.server.config.ConfigEntry;
 import org.opends.server.config.ConfigException;
 import org.opends.server.core.Operation;
@@ -98,22 +100,29 @@
    * unacceptable, then a human-readable explanation should be
    * appended to the provided buffer.
    *
-   * @param  password       The proposed clear-text password that
-   *                        should be validated.
-   * @param  operation      The operation that is being used to set
-   *                        the password.  It may be an add, a
-   *                        modify, or a password modify operation.
-   * @param  userEntry      The entry for the user whose password is
-   *                        being changed.
-   * @param  invalidReason  The buffer to which the human-readable
-   *                        explanation should be appended if it is
-   *                        determined that the password is not
-   *                        acceptable.
+   * @param  newPassword       The proposed clear-text password that
+   *                           should be validated.
+   * @param  currentPasswords  The set of clear-text current passwords
+   *                           for the user (if available).  Note that
+   *                           the current passwords may not always be
+   *                           available, and this may not comprise
+   *                           entire set of passwords currently
+   *                           for the user.
+   * @param  operation         The operation that is being used to set
+   *                           the password.  It may be an add, a
+   *                           modify, or a password modify operation.
+   * @param  userEntry         The entry for the user whose password
+   *                           is being changed.
+   * @param  invalidReason     The buffer to which the human-readable
+   *                           explanation should be appended if it is
+   *                           determined that the password is not
+   *                           acceptable.
    *
    * @return  <CODE>true</CODE> if the password is acceptable, or
    *          <CODE>false</CODE> if not.
    */
-  public abstract boolean passwordIsValid(ByteString password,
+  public abstract boolean passwordIsAcceptable(ByteString newPassword,
+                               Set<ByteString> currentPasswords,
                                Operation operation,
                                Entry userEntry,
                                StringBuilder invalidReason);
diff --git a/opendj-sdk/opends/src/server/org/opends/server/core/AddOperation.java b/opendj-sdk/opends/src/server/org/opends/server/core/AddOperation.java
index 26510ee..249dfe9 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/core/AddOperation.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/core/AddOperation.java
@@ -2298,12 +2298,14 @@
       // validation should be performed for administrators.
       if (! passwordPolicy.skipValidationForAdministrators())
       {
+        // There are never any current passwords for an add operation.
+        HashSet<ByteString> currentPasswords = new HashSet<ByteString>(0);
         StringBuilder invalidReason = new StringBuilder();
         for (PasswordValidator validator :
              passwordPolicy.getPasswordValidators().values())
         {
-          if (! validator.passwordIsValid(value, this, userEntry,
-                                          invalidReason))
+          if (! validator.passwordIsAcceptable(value, currentPasswords, this,
+                                               userEntry, invalidReason))
           {
             int    msgID   = MSGID_PWPOLICY_VALIDATION_FAILED;
             String message = getMessage(msgID, passwordAttribute.getNameOrOID(),
diff --git a/opendj-sdk/opends/src/server/org/opends/server/core/ModifyOperation.java b/opendj-sdk/opends/src/server/org/opends/server/core/ModifyOperation.java
index 79f8743..5cb7369 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/core/ModifyOperation.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/core/ModifyOperation.java
@@ -29,6 +29,8 @@
 
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.LinkedList;
@@ -2190,11 +2192,54 @@
           {
             if (newPasswords != null)
             {
+              HashSet<ByteString> clearPasswords = new HashSet<ByteString>();
+              clearPasswords.addAll(pwPolicyState.getClearPasswords());
+
+              if (currentPasswords != null)
+              {
+                if (clearPasswords.isEmpty())
+                {
+                  for (AttributeValue v : currentPasswords)
+                  {
+                    clearPasswords.add(v.getValue());
+                  }
+                }
+                else
+                {
+                  // NOTE:  We can't rely on the fact that Set doesn't allow
+                  // duplicates because technically it's possible that the
+                  // values aren't duplicates if they are ASN.1 elements with
+                  // different types (like 0x04 for a standard universal octet
+                  // string type versus 0x80 for a simple password in a bind
+                  // operation).  So we have to manually check for duplicates.
+                  for (AttributeValue v : currentPasswords)
+                  {
+                    ByteString pw = v.getValue();
+
+                    boolean found = false;
+                    for (ByteString s : clearPasswords)
+                    {
+                      if (Arrays.equals(s.value(), pw.value()))
+                      {
+                        found = true;
+                        break;
+                      }
+                    }
+
+                    if (! found)
+                    {
+                      clearPasswords.add(pw);
+                    }
+                  }
+                }
+              }
+
               for (AttributeValue v : newPasswords)
               {
                 StringBuilder invalidReason = new StringBuilder();
                 if (! pwPolicyState.passwordIsAcceptable(this, modifiedEntry,
                                                          v.getValue(),
+                                                         clearPasswords,
                                                          invalidReason))
                 {
                   setResultCode(ResultCode.UNWILLING_TO_PERFORM);
diff --git a/opendj-sdk/opends/src/server/org/opends/server/core/PasswordPolicyState.java b/opendj-sdk/opends/src/server/org/opends/server/core/PasswordPolicyState.java
index 4307311..c30c17f 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/core/PasswordPolicyState.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/core/PasswordPolicyState.java
@@ -36,6 +36,7 @@
 import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Set;
 
 import org.opends.server.api.AccountStatusNotificationHandler;
 import org.opends.server.api.PasswordGenerator;
@@ -3758,17 +3759,22 @@
    * Indicates whether the provided password appears to be acceptable according
    * to the password validators.
    *
-   * @param  operation      The operation that provided the password.
-   * @param  userEntry      The user entry in which the password is used.
-   * @param  password       The password to be validated.
-   * @param  invalidReason  A buffer that may be used to hold the invalid reason
-   *                        if the password is rejected.
+   * @param  operation         The operation that provided the password.
+   * @param  userEntry         The user entry in which the password is used.
+   * @param  newPassword       The password to be validated.
+   * @param  currentPasswords  The set of clear-text current passwords for the
+   *                           user (this may be a subset if not all of them are
+   *                           available in the clear, or empty if none of them
+   *                           are available in the clear).
+   * @param  invalidReason     A buffer that may be used to hold the invalid
+   *                           reason if the password is rejected.
    *
    * @return  <CODE>true</CODE> if the password is acceptable for use, or
    *          <CODE>false</CODE> if it is not.
    */
   public boolean passwordIsAcceptable(Operation operation, Entry userEntry,
-                                      ByteString password,
+                                      ByteString newPassword,
+                                      Set<ByteString> currentPasswords,
                                       StringBuilder invalidReason)
   {
     assert debugEnter(CLASS_NAME, "passwordIsAcceptable",
@@ -3780,8 +3786,8 @@
       PasswordValidator validator =
            passwordPolicy.getPasswordValidators().get(validatorDN);
 
-      if (! validator.passwordIsValid(password, operation, userEntry,
-                                      invalidReason))
+      if (! validator.passwordIsAcceptable(newPassword, currentPasswords,
+                                           operation, userEntry, invalidReason))
       {
         if (debug)
         {
diff --git a/opendj-sdk/opends/src/server/org/opends/server/extensions/LengthBasedPasswordValidator.java b/opendj-sdk/opends/src/server/org/opends/server/extensions/LengthBasedPasswordValidator.java
index 15c76b5..8bad509 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/extensions/LengthBasedPasswordValidator.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/extensions/LengthBasedPasswordValidator.java
@@ -31,6 +31,7 @@
 import java.util.ArrayList;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Set;
 
 import org.opends.server.api.ConfigurableComponent;
 import org.opends.server.api.PasswordValidator;
@@ -98,19 +99,9 @@
 
 
   /**
-   * Initializes this password validator based on the information in the
-   * provided configuration entry.
-   *
-   * @param  configEntry  The configuration entry that contains the information
-   *                      to use to initialize this password validator.
-   *
-   * @throws  ConfigException  If an unrecoverable problem arises in the
-   *                           process of performing the initialization.
-   *
-   * @throws  InitializationException  If a problem occurs during initialization
-   *                                   that is not related to the server
-   *                                   configuration.
+   * {@inheritDoc}
    */
+  @Override()
   public void initializePasswordValidator(ConfigEntry configEntry)
          throws ConfigException, InitializationException
   {
@@ -190,9 +181,9 @@
 
 
   /**
-   * Performs any finalization that might be required when this password
-   * validator is unloaded.  No action is taken in the default implementation.
+   * {@inheritDoc}
    */
+  @Override()
   public void finalizePasswordValidator()
   {
     assert debugEnter(CLASS_NAME, "finalizePasswordValidator");
@@ -203,33 +194,21 @@
 
 
   /**
-   * Indicates whether the provided password is acceptable for use by the
-   * specified user.  If the password is determined to be unacceptable, then a
-   * human-readable explanation should be appended to the provided buffer.
-   *
-   * @param  password       The proposed clear-text password that should be
-   *                        validated.
-   * @param  operation      The operation that is being used to set the
-   *                        password.
-   * @param  userEntry      The entry for the user whose password is being
-   *                        changed.
-   * @param  invalidReason  The buffer to which the human-readable explanation
-   *                        should be appended if it is determined that the
-   *                        password is not acceptable.
-   *
-   * @return  <CODE>true</CODE> if the password is acceptable, or
-   *          <CODE>false</CODE> if not.
+   * {@inheritDoc}
    */
-  public boolean passwordIsValid(ByteString password, Operation operation,
-                                 Entry userEntry, StringBuilder invalidReason)
+  @Override()
+  public boolean passwordIsAcceptable(ByteString newPassword,
+                                      Set<ByteString> currentPasswords,
+                                      Operation operation, Entry userEntry,
+                                      StringBuilder invalidReason)
   {
-    assert debugEnter(CLASS_NAME, "passwordIsValid",
+    assert debugEnter(CLASS_NAME, "passwordIsAcceptable",
                       "org.opends.server.protocols.asn1.ASN1OctetString",
                       String.valueOf(operation), String.valueOf(userEntry),
                       "java.lang.StringBuilder");
 
 
-    int numChars = password.stringValue().length();
+    int numChars = newPassword.stringValue().length();
 
     if ((minLength > 0) && (numChars < minLength))
     {
@@ -251,11 +230,7 @@
 
 
   /**
-   * Retrieves the DN of the configuration entry with which this component is
-   * associated.
-   *
-   * @return  The DN of the configuration entry with which this component is
-   *          associated.
+   * {@inheritDoc}
    */
   public DN getConfigurableComponentEntryDN()
   {
@@ -267,11 +242,7 @@
 
 
   /**
-   * Retrieves the set of configuration attributes that are associated with this
-   * configurable component.
-   *
-   * @return  The set of configuration attributes that are associated with this
-   *          configurable component.
+   * {@inheritDoc}
    */
   public List<ConfigAttribute> getConfigurationAttributes()
   {
@@ -296,18 +267,7 @@
 
 
   /**
-   * Indicates whether the provided configuration entry has an acceptable
-   * configuration for this component.  If it does not, then detailed
-   * information about the problem(s) should be added to the provided list.
-   *
-   * @param  configEntry          The configuration entry for which to make the
-   *                              determination.
-   * @param  unacceptableReasons  A list that can be used to hold messages about
-   *                              why the provided entry does not have an
-   *                              acceptable configuration.
-   *
-   * @return  <CODE>true</CODE> if the provided entry has an acceptable
-   *          configuration for this component, or <CODE>false</CODE> if not.
+   * {@inheritDoc}
    */
   public boolean hasAcceptableConfiguration(ConfigEntry configEntry,
                                             List<String> unacceptableReasons)
@@ -387,20 +347,7 @@
 
 
   /**
-   * Makes a best-effort attempt to apply the configuration contained in the
-   * provided entry.  Information about the result of this processing should be
-   * added to the provided message list.  Information should always be added to
-   * this list if a configuration change could not be applied.  If detailed
-   * results are requested, then information about the changes applied
-   * successfully (and optionally about parameters that were not changed) should
-   * also be included.
-   *
-   * @param  configEntry      The entry containing the new configuration to
-   *                          apply for this component.
-   * @param  detailedResults  Indicates whether detailed information about the
-   *                          processing should be added to the list.
-   *
-   * @return  Information about the result of the configuration update.
+   * {@inheritDoc}
    */
   public ConfigChangeResult applyNewConfiguration(ConfigEntry configEntry,
                                                   boolean detailedResults)
diff --git a/opendj-sdk/opends/src/server/org/opends/server/extensions/PasswordModifyExtendedOperation.java b/opendj-sdk/opends/src/server/org/opends/server/extensions/PasswordModifyExtendedOperation.java
index 664c67c..82f4b81 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/extensions/PasswordModifyExtendedOperation.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/extensions/PasswordModifyExtendedOperation.java
@@ -29,9 +29,11 @@
 
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.HashSet;
 import java.util.concurrent.locks.Lock;
 
 import org.opends.server.api.ClientConnection;
@@ -685,9 +687,29 @@
         {
           if (selfChange || (! pwPolicyState.skipValidationForAdministrators()))
           {
+            HashSet<ByteString> clearPasswords;
+            if (oldPassword == null)
+            {
+              clearPasswords =
+                   new HashSet<ByteString>(pwPolicyState.getClearPasswords());
+            }
+            else
+            {
+              clearPasswords = new HashSet<ByteString>();
+              clearPasswords.add(oldPassword);
+              for (ByteString pw : pwPolicyState.getClearPasswords())
+              {
+                if (! Arrays.equals(pw.value(), oldPassword.value()))
+                {
+                  clearPasswords.add(pw);
+                }
+              }
+            }
+
             StringBuilder invalidReason = new StringBuilder();
             if (! pwPolicyState.passwordIsAcceptable(operation, userEntry,
                                                      newPassword,
+                                                     clearPasswords,
                                                      invalidReason))
             {
               if (oldPassword == null)
diff --git a/opendj-sdk/opends/tests/unit-tests-testng/resource/config-changes.ldif b/opendj-sdk/opends/tests/unit-tests-testng/resource/config-changes.ldif
index 2c7307c..dcb8836 100644
--- a/opendj-sdk/opends/tests/unit-tests-testng/resource/config-changes.ldif
+++ b/opendj-sdk/opends/tests/unit-tests-testng/resource/config-changes.ldif
@@ -21,6 +21,19 @@
 replace: ds-cfg-suppress-internal-operations
 ds-cfg-suppress-internal-operations: false
 
+dn: cn=Test Password Validator,cn=Password Validators,cn=config
+changetype: add
+objectClass: top
+objectClass: ds-cfg-password-validator
+cn: Test Password Validator
+ds-cfg-password-validator-class: org.opends.server.extensions.TestPasswordValidator
+ds-cfg-password-validator-enabled: true
+
+dn: cn=Default Password Policy,cn=Password Policies,cn=config
+changetype: modify
+add: ds-cfg-password-validator-dn
+ds-cfg-password-validator-dn: cn=Test Password Validator,cn=Password Validators,cn=config
+
 dn: cn=SSHA512 UserPassword Policy,cn=Password Policies,cn=config
 changetype: add
 objectClass: top
@@ -49,6 +62,7 @@
 ds-cfg-require-secure-authentication: false
 ds-cfg-require-secure-password-changes: false
 ds-cfg-skip-validation-for-administrators: false
+ds-cfg-password-validator-dn: cn=Test Password Validator,cn=Password Validators,cn=config
 
 dn: cn=SHA1 AuthPassword Policy,cn=Password Policies,cn=config
 changetype: add
@@ -78,6 +92,7 @@
 ds-cfg-require-secure-authentication: false
 ds-cfg-require-secure-password-changes: false
 ds-cfg-skip-validation-for-administrators: false
+ds-cfg-password-validator-dn: cn=Test Password Validator,cn=Password Validators,cn=config
 
 dn: cn=Clear UserPassword Policy,cn=Password Policies,cn=config
 changetype: add
@@ -107,6 +122,7 @@
 ds-cfg-require-secure-authentication: false
 ds-cfg-require-secure-password-changes: false
 ds-cfg-skip-validation-for-administrators: false
+ds-cfg-password-validator-dn: cn=Test Password Validator,cn=Password Validators,cn=config
 
 dn: cn=Delay PreOperation Plugin,cn=Plugins,cn=config
 changetype: add
diff --git a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/api/PasswordValidatorTestCase.java b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/api/PasswordValidatorTestCase.java
new file mode 100644
index 0000000..140fdff
--- /dev/null
+++ b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/api/PasswordValidatorTestCase.java
@@ -0,0 +1,866 @@
+/*
+ * 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
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE
+ * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
+ * 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
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  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
+ *
+ *
+ *      Portions Copyright 2006 Sun Microsystems, Inc.
+ */
+package org.opends.server.api;
+
+
+
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.Set;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import org.opends.server.TestCaseUtils;
+import org.opends.server.core.AddOperation;
+import org.opends.server.extensions.TestPasswordValidator;
+import org.opends.server.protocols.asn1.ASN1OctetString;
+import org.opends.server.protocols.asn1.ASN1Reader;
+import org.opends.server.protocols.asn1.ASN1Sequence;
+import org.opends.server.protocols.asn1.ASN1Writer;
+import org.opends.server.protocols.internal.InternalClientConnection;
+import org.opends.server.protocols.ldap.BindRequestProtocolOp;
+import org.opends.server.protocols.ldap.BindResponseProtocolOp;
+import org.opends.server.protocols.ldap.LDAPAttribute;
+import org.opends.server.protocols.ldap.LDAPMessage;
+import org.opends.server.protocols.ldap.LDAPModification;
+import org.opends.server.protocols.ldap.ModifyRequestProtocolOp;
+import org.opends.server.protocols.ldap.ModifyResponseProtocolOp;
+import org.opends.server.tools.LDAPPasswordModify;
+import org.opends.server.types.Attribute;
+import org.opends.server.types.ByteString;
+import org.opends.server.types.DN;
+import org.opends.server.types.Entry;
+import org.opends.server.types.ModificationType;
+import org.opends.server.types.ResultCode;
+
+import static org.testng.Assert.*;
+
+
+
+/**
+ * A set of generic test cases for password validators.
+ */
+public class PasswordValidatorTestCase
+       extends APITestCase
+{
+  /**
+   * Ensures that the Directory Server is running.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @BeforeClass()
+  public void startServer()
+         throws Exception
+  {
+    TestCaseUtils.startServer();
+  }
+
+
+
+  /**
+   * Gets simple test coverage for the default
+   * PasswordValidator.finalizePasswordValidator method.
+   */
+  @Test()
+  public void testFinalizePasswordValidator()
+  {
+    TestPasswordValidator.getInstance().finalizePasswordValidator();
+  }
+
+
+
+  /**
+   * Performs a test to ensure that the password validation will be successful
+   * under the base conditions for the password modify extended operation.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testSuccessfulValidationPasswordModifyExtOp()
+         throws Exception
+  {
+    TestPasswordValidator.setNextReturnValue(true);
+    TestPasswordValidator.setNextInvalidReason(null);
+
+    TestCaseUtils.initializeTestBackend(true);
+
+    Entry userEntry = TestCaseUtils.makeEntry(
+         "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");
+
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    AddOperation addOperation =
+         conn.processAdd(userEntry.getDN(), userEntry.getObjectClasses(),
+                         userEntry.getUserAttributes(),
+                         userEntry.getOperationalAttributes());
+    assertEquals(addOperation.getResultCode(), ResultCode.SUCCESS);
+
+
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "uid=test.user,o=test",
+      "-w", "password",
+      "-c", "password",
+      "-n", "newPassword"
+    };
+    assertEquals(LDAPPasswordModify.mainPasswordModify(args, false, null, null),
+                 0);
+
+    assertEquals(TestPasswordValidator.getLastNewPassword().value(),
+                 new ASN1OctetString("newPassword").value());
+    assertFalse(TestPasswordValidator.getLastCurrentPasswords().isEmpty());
+  }
+
+
+
+  /**
+   * Performs a test to ensure that the password validation will fail if the
+   * test validator is configured to make it fail for the password modify
+   * extended operation.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testFailedValidationPasswordModifyExtOp()
+         throws Exception
+  {
+    TestPasswordValidator.setNextReturnValue(true);
+    TestPasswordValidator.setNextInvalidReason(null);
+
+    TestCaseUtils.initializeTestBackend(true);
+
+    Entry userEntry = TestCaseUtils.makeEntry(
+         "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");
+
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    AddOperation addOperation =
+         conn.processAdd(userEntry.getDN(), userEntry.getObjectClasses(),
+                         userEntry.getUserAttributes(),
+                         userEntry.getOperationalAttributes());
+    assertEquals(addOperation.getResultCode(), ResultCode.SUCCESS);
+
+
+    TestPasswordValidator.setNextReturnValue(false);
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "uid=test.user,o=test",
+      "-w", "password",
+      "-c", "password",
+      "-n", "newPassword"
+    };
+
+    int returnCode = LDAPPasswordModify.mainPasswordModify(args, false, null,
+                                                           null);
+    assertFalse(returnCode == 0);
+
+    assertEquals(TestPasswordValidator.getLastNewPassword().value(),
+                 new ASN1OctetString("newPassword").value());
+    assertFalse(TestPasswordValidator.getLastCurrentPasswords().isEmpty());
+
+    TestPasswordValidator.setNextReturnValue(true);
+  }
+
+
+
+  /**
+   * Performs a test to make sure that the clear-text password will not be
+   * provided if the user has a non-reversible scheme and does not provide the
+   * current password for a password modify extended operation.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testCurrentPasswordNotAvailablePasswordModifyExtOp()
+         throws Exception
+  {
+    TestPasswordValidator.setNextReturnValue(true);
+    TestPasswordValidator.setNextInvalidReason(null);
+
+    TestCaseUtils.initializeTestBackend(true);
+
+    Entry userEntry = TestCaseUtils.makeEntry(
+         "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");
+
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    AddOperation addOperation =
+         conn.processAdd(userEntry.getDN(), userEntry.getObjectClasses(),
+                         userEntry.getUserAttributes(),
+                         userEntry.getOperationalAttributes());
+    assertEquals(addOperation.getResultCode(), ResultCode.SUCCESS);
+
+
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "uid=test.user,o=test",
+      "-w", "password",
+      "-n", "newPassword"
+    };
+    assertEquals(LDAPPasswordModify.mainPasswordModify(args, false, null, null),
+                 0);
+
+    Set<ByteString> currentPasswords =
+         TestPasswordValidator.getLastCurrentPasswords();
+    assertTrue(currentPasswords.isEmpty());
+  }
+
+
+
+  /**
+   * Performs a test to make sure that the clear-text password will be provided
+   * if the user has a non-reversible scheme but provides the current password
+   * for a password modify extended operation.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testCurrentPasswordAvailablePasswordModifyExtOp()
+         throws Exception
+  {
+    TestPasswordValidator.setNextReturnValue(true);
+    TestPasswordValidator.setNextInvalidReason(null);
+
+    TestCaseUtils.initializeTestBackend(true);
+
+    Entry userEntry = TestCaseUtils.makeEntry(
+         "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");
+
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    AddOperation addOperation =
+         conn.processAdd(userEntry.getDN(), userEntry.getObjectClasses(),
+                         userEntry.getUserAttributes(),
+                         userEntry.getOperationalAttributes());
+    assertEquals(addOperation.getResultCode(), ResultCode.SUCCESS);
+
+
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "uid=test.user,o=test",
+      "-w", "password",
+      "-c", "password",
+      "-n", "newPassword"
+    };
+    assertEquals(LDAPPasswordModify.mainPasswordModify(args, false, null, null),
+                 0);
+
+    Set<ByteString> currentPasswords =
+         TestPasswordValidator.getLastCurrentPasswords();
+    assertFalse(currentPasswords.isEmpty());
+    assertEquals(currentPasswords.size(), 1);
+    assertEquals(currentPasswords.iterator().next().value(),
+                 new ASN1OctetString("password").value());
+  }
+
+
+
+  /**
+   * Performs a test to make sure that the clear-text password will be provided
+   * if the user has a reversible scheme and does not provide the current
+   * password for a password modify extended operation.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testStoredPasswordAvailablePasswordModifyExtOp()
+         throws Exception
+  {
+    TestPasswordValidator.setNextReturnValue(true);
+    TestPasswordValidator.setNextInvalidReason(null);
+
+    TestCaseUtils.initializeTestBackend(true);
+
+    Entry userEntry = TestCaseUtils.makeEntry(
+         "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",
+         "pwdPolicySubentry: cn=Clear UserPassword Policy,cn=Password " +
+              "Policies,cn=config");
+
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    AddOperation addOperation =
+         conn.processAdd(userEntry.getDN(), userEntry.getObjectClasses(),
+                         userEntry.getUserAttributes(),
+                         userEntry.getOperationalAttributes());
+    assertEquals(addOperation.getResultCode(), ResultCode.SUCCESS);
+
+
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "uid=test.user,o=test",
+      "-w", "password",
+      "-n", "newPassword"
+    };
+    assertEquals(LDAPPasswordModify.mainPasswordModify(args, false, null, null),
+                 0);
+
+    Set<ByteString> currentPasswords =
+         TestPasswordValidator.getLastCurrentPasswords();
+    assertFalse(currentPasswords.isEmpty());
+    assertEquals(currentPasswords.size(), 1);
+    assertEquals(currentPasswords.iterator().next().value(),
+                 new ASN1OctetString("password").value());
+  }
+
+
+
+  /**
+   * Performs a test to make sure that the clear-text password will be provided
+   * if the user has a reversible scheme and also provides the current password
+   * for a password modify extended operation.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testStoredAndCurrentPasswordAvailablePasswordModifyExtOp()
+         throws Exception
+  {
+    TestPasswordValidator.setNextReturnValue(true);
+    TestPasswordValidator.setNextInvalidReason(null);
+
+    TestCaseUtils.initializeTestBackend(true);
+
+    Entry userEntry = TestCaseUtils.makeEntry(
+         "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",
+         "pwdPolicySubentry: cn=Clear UserPassword Policy,cn=Password " +
+              "Policies,cn=config");
+
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    AddOperation addOperation =
+         conn.processAdd(userEntry.getDN(), userEntry.getObjectClasses(),
+                         userEntry.getUserAttributes(),
+                         userEntry.getOperationalAttributes());
+    assertEquals(addOperation.getResultCode(), ResultCode.SUCCESS);
+
+
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "uid=test.user,o=test",
+      "-w", "password",
+      "-c", "password",
+      "-n", "newPassword"
+    };
+    assertEquals(LDAPPasswordModify.mainPasswordModify(args, false, null, null),
+                 0);
+
+    Set<ByteString> currentPasswords =
+         TestPasswordValidator.getLastCurrentPasswords();
+    assertFalse(currentPasswords.isEmpty());
+    assertEquals(currentPasswords.size(), 1);
+    assertEquals(currentPasswords.iterator().next().value(),
+                 new ASN1OctetString("password").value());
+  }
+
+
+
+  /**
+   * Performs a test to ensure that the password validation will be successful
+   * under the base conditions for the modify operation.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testSuccessfulValidationModify()
+         throws Exception
+  {
+    TestPasswordValidator.setNextReturnValue(true);
+    TestPasswordValidator.setNextInvalidReason(null);
+
+    TestCaseUtils.initializeTestBackend(true);
+
+    Entry userEntry = TestCaseUtils.makeEntry(
+         "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");
+
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    AddOperation addOperation =
+         conn.processAdd(userEntry.getDN(), userEntry.getObjectClasses(),
+                         userEntry.getUserAttributes(),
+                         userEntry.getOperationalAttributes());
+    assertEquals(addOperation.getResultCode(), ResultCode.SUCCESS);
+
+
+    Socket s = new Socket("127.0.0.1", (int) TestCaseUtils.getServerLdapPort());
+    ASN1Reader r = new ASN1Reader(s);
+    ASN1Writer w = new ASN1Writer(s);
+    r.setIOTimeout(3000);
+
+    BindRequestProtocolOp bindRequest =
+      new BindRequestProtocolOp(
+               new ASN1OctetString("uid=test.user,o=test"),
+                                3, new ASN1OctetString("password"));
+    LDAPMessage message = new LDAPMessage(1, bindRequest);
+    w.writeElement(message.encode());
+
+    message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+    BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
+    assertEquals(bindResponse.getResultCode(), 0);
+
+    ArrayList<LDAPModification> mods = new ArrayList<LDAPModification>();
+    ArrayList<ASN1OctetString> values = new ArrayList<ASN1OctetString>();
+    values.add(new ASN1OctetString("newPassword"));
+    LDAPAttribute attr = new LDAPAttribute("userPassword", values);
+    mods.add(new LDAPModification(ModificationType.REPLACE, attr));
+
+    ModifyRequestProtocolOp modifyRequest =
+         new ModifyRequestProtocolOp(
+                  new ASN1OctetString("uid=test.user,o=test"), mods);
+    message = new LDAPMessage(2, modifyRequest);
+    w.writeElement(message.encode());
+
+    message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+    ModifyResponseProtocolOp modifyResponse =
+         message.getModifyResponseProtocolOp();
+    assertEquals(modifyResponse.getResultCode(), 0);
+
+    assertEquals(TestPasswordValidator.getLastNewPassword().value(),
+                 new ASN1OctetString("newPassword").value());
+    assertTrue(TestPasswordValidator.getLastCurrentPasswords().isEmpty());
+  }
+
+
+
+  /**
+   * Performs a test to ensure that the password validation will fail if the
+   * test validator is configured to make it fail for the modify operation.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testFailedValidationModify()
+         throws Exception
+  {
+    TestPasswordValidator.setNextReturnValue(true);
+    TestPasswordValidator.setNextInvalidReason(null);
+
+    TestCaseUtils.initializeTestBackend(true);
+
+    Entry userEntry = TestCaseUtils.makeEntry(
+         "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");
+
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    AddOperation addOperation =
+         conn.processAdd(userEntry.getDN(), userEntry.getObjectClasses(),
+                         userEntry.getUserAttributes(),
+                         userEntry.getOperationalAttributes());
+    assertEquals(addOperation.getResultCode(), ResultCode.SUCCESS);
+
+
+    Socket s = new Socket("127.0.0.1", (int) TestCaseUtils.getServerLdapPort());
+    ASN1Reader r = new ASN1Reader(s);
+    ASN1Writer w = new ASN1Writer(s);
+    r.setIOTimeout(3000);
+
+    BindRequestProtocolOp bindRequest =
+      new BindRequestProtocolOp(
+               new ASN1OctetString("uid=test.user,o=test"),
+                                3, new ASN1OctetString("password"));
+    LDAPMessage message = new LDAPMessage(1, bindRequest);
+    w.writeElement(message.encode());
+
+    message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+    BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
+    assertEquals(bindResponse.getResultCode(), 0);
+
+    ArrayList<LDAPModification> mods = new ArrayList<LDAPModification>();
+    ArrayList<ASN1OctetString> values = new ArrayList<ASN1OctetString>();
+    values.add(new ASN1OctetString("newPassword"));
+    LDAPAttribute attr = new LDAPAttribute("userPassword", values);
+    mods.add(new LDAPModification(ModificationType.REPLACE, attr));
+
+    TestPasswordValidator.setNextReturnValue(false);
+    ModifyRequestProtocolOp modifyRequest =
+         new ModifyRequestProtocolOp(
+                  new ASN1OctetString("uid=test.user,o=test"), mods);
+    message = new LDAPMessage(2, modifyRequest);
+    w.writeElement(message.encode());
+
+    message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+    ModifyResponseProtocolOp modifyResponse =
+         message.getModifyResponseProtocolOp();
+    assertFalse(modifyResponse.getResultCode() == 0);
+
+    assertEquals(TestPasswordValidator.getLastNewPassword().value(),
+                 new ASN1OctetString("newPassword").value());
+    assertTrue(TestPasswordValidator.getLastCurrentPasswords().isEmpty());
+
+    TestPasswordValidator.setNextReturnValue(true);
+  }
+
+
+
+  /**
+   * Performs a test to make sure that the clear-text password will be provided
+   * if the user has a non-reversible scheme but provides the current password
+   * for a modify operation.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testCurrentPasswordAvailableModify()
+         throws Exception
+  {
+    TestPasswordValidator.setNextReturnValue(true);
+    TestPasswordValidator.setNextInvalidReason(null);
+
+    TestCaseUtils.initializeTestBackend(true);
+
+    Entry userEntry = TestCaseUtils.makeEntry(
+         "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");
+
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    AddOperation addOperation =
+         conn.processAdd(userEntry.getDN(), userEntry.getObjectClasses(),
+                         userEntry.getUserAttributes(),
+                         userEntry.getOperationalAttributes());
+    assertEquals(addOperation.getResultCode(), ResultCode.SUCCESS);
+
+
+    Socket s = new Socket("127.0.0.1", (int) TestCaseUtils.getServerLdapPort());
+    ASN1Reader r = new ASN1Reader(s);
+    ASN1Writer w = new ASN1Writer(s);
+    r.setIOTimeout(3000);
+
+    BindRequestProtocolOp bindRequest =
+      new BindRequestProtocolOp(
+               new ASN1OctetString("uid=test.user,o=test"),
+                                3, new ASN1OctetString("password"));
+    LDAPMessage message = new LDAPMessage(1, bindRequest);
+    w.writeElement(message.encode());
+
+    message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+    BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
+    assertEquals(bindResponse.getResultCode(), 0);
+
+    ArrayList<LDAPModification> mods = new ArrayList<LDAPModification>();
+    ArrayList<ASN1OctetString> values = new ArrayList<ASN1OctetString>();
+    values.add(new ASN1OctetString("password"));
+    LDAPAttribute attr = new LDAPAttribute("userPassword", values);
+    mods.add(new LDAPModification(ModificationType.DELETE, attr));
+
+    values = new ArrayList<ASN1OctetString>();
+    values.add(new ASN1OctetString("newPassword"));
+    attr = new LDAPAttribute("userPassword", values);
+    mods.add(new LDAPModification(ModificationType.ADD, attr));
+
+    ModifyRequestProtocolOp modifyRequest =
+         new ModifyRequestProtocolOp(
+                  new ASN1OctetString("uid=test.user,o=test"), mods);
+    message = new LDAPMessage(2, modifyRequest);
+    w.writeElement(message.encode());
+
+    message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+    ModifyResponseProtocolOp modifyResponse =
+         message.getModifyResponseProtocolOp();
+    assertEquals(modifyResponse.getResultCode(), 0);
+
+    Set<ByteString> currentPasswords =
+         TestPasswordValidator.getLastCurrentPasswords();
+    assertFalse(currentPasswords.isEmpty());
+    assertEquals(currentPasswords.size(), 1);
+    assertEquals(currentPasswords.iterator().next().value(),
+                 new ASN1OctetString("password").value());
+  }
+
+
+
+  /**
+   * Performs a test to make sure that the clear-text password will be provided
+   * if the user has a reversible scheme and does not provide the current
+   * password for a modify operation.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testStoredPasswordAvailableModify()
+         throws Exception
+  {
+    TestPasswordValidator.setNextReturnValue(true);
+    TestPasswordValidator.setNextInvalidReason(null);
+
+    TestCaseUtils.initializeTestBackend(true);
+
+    Entry userEntry = TestCaseUtils.makeEntry(
+         "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",
+         "pwdPolicySubentry: cn=Clear UserPassword Policy,cn=Password " +
+              "Policies,cn=config");
+
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    AddOperation addOperation =
+         conn.processAdd(userEntry.getDN(), userEntry.getObjectClasses(),
+                         userEntry.getUserAttributes(),
+                         userEntry.getOperationalAttributes());
+    assertEquals(addOperation.getResultCode(), ResultCode.SUCCESS);
+
+
+    Socket s = new Socket("127.0.0.1", (int) TestCaseUtils.getServerLdapPort());
+    ASN1Reader r = new ASN1Reader(s);
+    ASN1Writer w = new ASN1Writer(s);
+    r.setIOTimeout(3000);
+
+    BindRequestProtocolOp bindRequest =
+      new BindRequestProtocolOp(
+               new ASN1OctetString("uid=test.user,o=test"),
+                                3, new ASN1OctetString("password"));
+    LDAPMessage message = new LDAPMessage(1, bindRequest);
+    w.writeElement(message.encode());
+
+    message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+    BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
+    assertEquals(bindResponse.getResultCode(), 0);
+
+    ArrayList<LDAPModification> mods = new ArrayList<LDAPModification>();
+    ArrayList<ASN1OctetString> values = new ArrayList<ASN1OctetString>();
+    values.add(new ASN1OctetString("newPassword"));
+    LDAPAttribute attr = new LDAPAttribute("userPassword", values);
+    mods.add(new LDAPModification(ModificationType.REPLACE, attr));
+
+    ModifyRequestProtocolOp modifyRequest =
+         new ModifyRequestProtocolOp(
+                  new ASN1OctetString("uid=test.user,o=test"), mods);
+    message = new LDAPMessage(2, modifyRequest);
+    w.writeElement(message.encode());
+
+    message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+    ModifyResponseProtocolOp modifyResponse =
+         message.getModifyResponseProtocolOp();
+    assertEquals(modifyResponse.getResultCode(), 0);
+
+    Set<ByteString> currentPasswords =
+         TestPasswordValidator.getLastCurrentPasswords();
+    assertFalse(currentPasswords.isEmpty());
+    assertEquals(currentPasswords.size(), 1);
+    assertEquals(currentPasswords.iterator().next().value(),
+                 new ASN1OctetString("password").value());
+  }
+
+
+
+  /**
+   * Performs a test to make sure that the clear-text password will be provided
+   * if the user has a reversible scheme and also provides the current password
+   * for a modify operation.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testStoredAndCurrentPasswordAvailableModify()
+         throws Exception
+  {
+    TestPasswordValidator.setNextReturnValue(true);
+    TestPasswordValidator.setNextInvalidReason(null);
+
+    TestCaseUtils.initializeTestBackend(true);
+
+    Entry userEntry = TestCaseUtils.makeEntry(
+         "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",
+         "pwdPolicySubentry: cn=Clear UserPassword Policy,cn=Password " +
+              "Policies,cn=config");
+
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    AddOperation addOperation =
+         conn.processAdd(userEntry.getDN(), userEntry.getObjectClasses(),
+                         userEntry.getUserAttributes(),
+                         userEntry.getOperationalAttributes());
+    assertEquals(addOperation.getResultCode(), ResultCode.SUCCESS);
+
+
+    Socket s = new Socket("127.0.0.1", (int) TestCaseUtils.getServerLdapPort());
+    ASN1Reader r = new ASN1Reader(s);
+    ASN1Writer w = new ASN1Writer(s);
+    r.setIOTimeout(3000);
+
+    BindRequestProtocolOp bindRequest =
+      new BindRequestProtocolOp(
+               new ASN1OctetString("uid=test.user,o=test"),
+                                3, new ASN1OctetString("password"));
+    LDAPMessage message = new LDAPMessage(1, bindRequest);
+    w.writeElement(message.encode());
+
+    message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+    BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
+    assertEquals(bindResponse.getResultCode(), 0);
+
+    ArrayList<LDAPModification> mods = new ArrayList<LDAPModification>();
+    ArrayList<ASN1OctetString> values = new ArrayList<ASN1OctetString>();
+    values.add(new ASN1OctetString("password"));
+    LDAPAttribute attr = new LDAPAttribute("userPassword", values);
+    mods.add(new LDAPModification(ModificationType.DELETE, attr));
+
+    values = new ArrayList<ASN1OctetString>();
+    values.add(new ASN1OctetString("newPassword"));
+    attr = new LDAPAttribute("userPassword", values);
+    mods.add(new LDAPModification(ModificationType.ADD, attr));
+
+    ModifyRequestProtocolOp modifyRequest =
+         new ModifyRequestProtocolOp(
+                  new ASN1OctetString("uid=test.user,o=test"), mods);
+    message = new LDAPMessage(2, modifyRequest);
+    w.writeElement(message.encode());
+
+    message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+    ModifyResponseProtocolOp modifyResponse =
+         message.getModifyResponseProtocolOp();
+    assertEquals(modifyResponse.getResultCode(), 0);
+
+    Set<ByteString> currentPasswords =
+         TestPasswordValidator.getLastCurrentPasswords();
+    assertFalse(currentPasswords.isEmpty());
+    assertEquals(currentPasswords.size(), 1);
+    assertEquals(currentPasswords.iterator().next().value(),
+                 new ASN1OctetString("password").value());
+  }
+}
+
diff --git a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/core/PasswordPolicyTestCase.java b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/core/PasswordPolicyTestCase.java
index 6b54dd5..7860581 100644
--- a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/core/PasswordPolicyTestCase.java
+++ b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/core/PasswordPolicyTestCase.java
@@ -2522,7 +2522,7 @@
   {
     PasswordPolicy p = DirectoryServer.getDefaultPasswordPolicy();
     assertNotNull(p.getPasswordValidators());
-    assertTrue(p.getPasswordValidators().isEmpty());
+    assertFalse(p.getPasswordValidators().isEmpty());
 
     String dnStr = "cn=Default Password Policy,cn=Password Policies,cn=config";
     String attr  = "ds-cfg-password-validator-dn";
@@ -2567,7 +2567,7 @@
                       "cn=config");
     PasswordPolicy p = DirectoryServer.getPasswordPolicy(dn);
     assertNotNull(p.getPasswordValidators());
-    assertTrue(p.getPasswordValidators().isEmpty());
+    assertFalse(p.getPasswordValidators().isEmpty());
 
     String attr  = "ds-cfg-password-validator-dn";
     String valDN = "cn=Length-Based Password Validator," +
diff --git a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LengthBasedPasswordValidatorTestCase.java b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LengthBasedPasswordValidatorTestCase.java
index 3c57f29..614bc88 100644
--- a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LengthBasedPasswordValidatorTestCase.java
+++ b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LengthBasedPasswordValidatorTestCase.java
@@ -29,6 +29,7 @@
 
 
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
 
 import org.testng.annotations.BeforeClass;
@@ -43,6 +44,7 @@
 import org.opends.server.protocols.asn1.ASN1OctetString;
 import org.opends.server.protocols.internal.InternalClientConnection;
 import org.opends.server.types.Attribute;
+import org.opends.server.types.ByteString;
 import org.opends.server.types.Control;
 import org.opends.server.types.DN;
 import org.opends.server.types.Entry;
@@ -312,13 +314,13 @@
 
 
   /**
-   * Tests the <CODE>passwordIsValid</CODE> method with no constraints on
+   * Tests the <CODE>passwordIsAcceptable</CODE> method with no constraints on
    * password length.
    *
    * @throws  Exception  If an unexpected problem occurs.
    */
   @Test()
-  public void testPasswordIsValidNoConstraints()
+  public void testPasswordIsAcceptableNoConstraints()
          throws Exception
   {
     TestCaseUtils.initializeTestBackend(true);
@@ -374,8 +376,9 @@
                                DN.decode("cn=uid=test.user,o=test"), mods);
 
       StringBuilder invalidReason = new StringBuilder();
-      assertTrue(validator.passwordIsValid(password, op, userEntry,
-                                           invalidReason));
+      assertTrue(validator.passwordIsAcceptable(password,
+                                                new HashSet<ByteString>(0),
+                                                op, userEntry, invalidReason));
     }
 
     validator.finalizePasswordValidator();
@@ -384,13 +387,13 @@
 
 
   /**
-   * Tests the <CODE>passwordIsValid</CODE> method with a constraint on the
+   * Tests the <CODE>passwordIsAcceptable</CODE> method with a constraint on the
    * minimum password length.
    *
    * @throws  Exception  If an unexpected problem occurs.
    */
   @Test()
-  public void testPasswordIsValidMinLengthConstraint()
+  public void testPasswordIsAcceptableMinLengthConstraint()
          throws Exception
   {
     TestCaseUtils.initializeTestBackend(true);
@@ -447,8 +450,10 @@
 
       StringBuilder invalidReason = new StringBuilder();
       assertEquals((buffer.length() >= 10),
-                   validator.passwordIsValid(password, op, userEntry,
-                                             invalidReason));
+                   validator.passwordIsAcceptable(password,
+                                                  new HashSet<ByteString>(0),
+                                                  op, userEntry,
+                                                  invalidReason));
     }
 
     validator.finalizePasswordValidator();
@@ -457,13 +462,13 @@
 
 
   /**
-   * Tests the <CODE>passwordIsValid</CODE> method with a constraint on the
+   * Tests the <CODE>passwordIsAcceptable</CODE> method with a constraint on the
    * maximum password length.
    *
    * @throws  Exception  If an unexpected problem occurs.
    */
   @Test()
-  public void testPasswordIsValidMaxLengthConstraint()
+  public void testPasswordIsAcceptableMaxLengthConstraint()
          throws Exception
   {
     TestCaseUtils.initializeTestBackend(true);
@@ -520,8 +525,10 @@
 
       StringBuilder invalidReason = new StringBuilder();
       assertEquals((buffer.length() <= 10),
-                   validator.passwordIsValid(password, op, userEntry,
-                                             invalidReason));
+                   validator.passwordIsAcceptable(password,
+                                                  new HashSet<ByteString>(0),
+                                                  op, userEntry,
+                                                  invalidReason));
     }
 
     validator.finalizePasswordValidator();
@@ -530,13 +537,13 @@
 
 
   /**
-   * Tests the <CODE>passwordIsValid</CODE> method with constraints on both the
-   * minimum and maximum password length.
+   * Tests the <CODE>passwordIsAcceptable</CODE> method with constraints on both
+   * the minimum and maximum password length.
    *
    * @throws  Exception  If an unexpected problem occurs.
    */
   @Test()
-  public void testPasswordIsValidMinAndMaxLengthConstraints()
+  public void testPasswordIsAcceptableMinAndMaxLengthConstraints()
          throws Exception
   {
     TestCaseUtils.initializeTestBackend(true);
@@ -593,8 +600,10 @@
 
       StringBuilder invalidReason = new StringBuilder();
       assertEquals(((buffer.length() >= 6) && (buffer.length() <= 10)),
-                   validator.passwordIsValid(password, op, userEntry,
-                                             invalidReason));
+                   validator.passwordIsAcceptable(password,
+                                                  new HashSet<ByteString>(0),
+                                                  op, userEntry,
+                                                  invalidReason));
     }
 
     validator.finalizePasswordValidator();
diff --git a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/TestPasswordValidator.java b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/TestPasswordValidator.java
new file mode 100644
index 0000000..a4af91a
--- /dev/null
+++ b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/TestPasswordValidator.java
@@ -0,0 +1,237 @@
+/*
+ * 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
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE
+ * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
+ * 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
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  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
+ *
+ *
+ *      Portions Copyright 2006 Sun Microsystems, Inc.
+ */
+package org.opends.server.extensions;
+
+
+
+import java.util.Set;
+
+import org.opends.server.api.PasswordValidator;
+import org.opends.server.config.ConfigEntry;
+import org.opends.server.core.Operation;
+import org.opends.server.types.ByteString;
+import org.opends.server.types.Entry;
+import org.opends.server.types.InitializationException;
+
+
+
+/**
+ * This class provides a very simple password validator that can be used for
+ * testing purposes.  It provides the ability to inspect the arguments provided
+ * to the password validator, as well as to manipulate the result that it will
+ * return.
+ */
+public class TestPasswordValidator
+       extends PasswordValidator
+{
+  /**
+   * The singleton instance of this test password validator.
+   */
+  private static TestPasswordValidator instance = null;
+
+
+
+  // The next value to return from the passwordIsAcceptable method.
+  private boolean nextReturnValue;
+
+  // The last new password provided to the passwordIsAcceptable method.
+  private ByteString lastNewPassword;
+
+  // The last user entry provided to the passwordIsAcceptable method.
+  private Entry lastUserEntry;
+
+  // The last operation provided to the passwordIsAcceptable method.
+  private Operation lastOperation;
+
+  // The last set of current passwords provided to the passwordIsAcceptable
+  // method.
+  private Set<ByteString> lastCurrentPasswords;
+
+  // The next invalid reason that should be used in the passwordIsAcceptable
+  // method.
+  private String nextInvalidReason;
+
+
+
+  /**
+   * Creates a new instance of this password validator.
+   */
+  public TestPasswordValidator()
+  {
+    super();
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void initializePasswordValidator(ConfigEntry configEntry)
+         throws InitializationException
+  {
+    if (instance == null)
+    {
+      instance = this;
+    }
+    else
+    {
+      throw new InitializationException(1,
+           "Cannot configure more than one TestPasswordValidator instance");
+    }
+
+    lastNewPassword      = null;
+    lastCurrentPasswords = null;
+    lastOperation        = null;
+    lastUserEntry        = null;
+    nextReturnValue      = true;
+    nextInvalidReason    = null;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public boolean passwordIsAcceptable(ByteString newPassword,
+                                      Set<ByteString> currentPasswords,
+                                      Operation operation, Entry userEntry,
+                                      StringBuilder invalidReason)
+  {
+    lastNewPassword      = newPassword;
+    lastCurrentPasswords = currentPasswords;
+    lastOperation        = operation;
+    lastUserEntry        = userEntry;
+
+    if (nextInvalidReason != null)
+    {
+      invalidReason.append(nextInvalidReason);
+    }
+
+    return nextReturnValue;
+  }
+
+
+
+  /**
+   * Retrieves an instance of this test password validator.
+   *
+   * @return  An instance of this test password validator, or <CODE>null</CODE>
+   *          if no instance has been created.
+   */
+  public static TestPasswordValidator getInstance()
+  {
+    return instance;
+  }
+
+
+
+  /**
+   * Retrieves the last <CODE>newPassword</CODE> value provided to the
+   * <CODE>passwordIsAcceptable</CODE> method.
+   *
+   * @return  The last <CODE>newPassword</CODE> value provided to the
+   *          <CODE>passwordIsAcceptable</CODE> method.
+   */
+  public static ByteString getLastNewPassword()
+  {
+    return instance.lastNewPassword;
+  }
+
+
+
+  /**
+   * Retrieves the last <CODE>currentPasswords</CODE> value provided to the
+   * <CODE>passwordIsAcceptable</CODE> method.
+   *
+   * @return  The last <CODE>currentPasswords</CODE> value provided to the
+   *          <CODE>passwordIsAcceptable</CODE> method.
+   */
+  public static Set<ByteString> getLastCurrentPasswords()
+  {
+    return instance.lastCurrentPasswords;
+  }
+
+
+
+  /**
+   * Retrieves the last <CODE>operation</CODE> value provided to the
+   * <CODE>passwordIsAcceptable</CODE> method.
+   *
+   * @return  The last <CODE>operation</CODE> value provided to the
+   *          <CODE>passwordIsAcceptable</CODE> method.
+   */
+  public static Operation getLastOperation()
+  {
+    return instance.lastOperation;
+  }
+
+
+
+  /**
+   * Retrieves the last <CODE>userEntry</CODE> value provided to the
+   * <CODE>passwordIsAcceptable</CODE> method.
+   *
+   * @return  The last <CODE>userEntry</CODE> value provided to the
+   *          <CODE>passwordIsAcceptable</CODE> method.
+   */
+  public static Entry getLastUserEntry()
+  {
+    return instance.lastUserEntry;
+  }
+
+
+
+  /**
+   * Sets the next value to return from the <CODE>passwordIsAcceptable</CODE>
+   * method.
+   *
+   * @param  nextReturnValue  The next value to return from the
+   *                          <CODE>passwordIsAcceptable</CODE> method.
+   */
+  public static void setNextReturnValue(boolean nextReturnValue)
+  {
+    instance.nextReturnValue = nextReturnValue;
+  }
+
+
+
+  /**
+   * Sets the next string to append to the <CODE>invalidReason</CODE> buffer in
+   * the <CODE>passwordIsAcceptable</CODE> method.
+   *
+   * @param  nextReturnValue  The next string to append to the
+   *                          <CODE>invalidReason</CODE> buffer in the
+   *                          <CODE>passwordIsAcceptable</CODE> method.
+   */
+  public static void setNextInvalidReason(String nextInvalidReason)
+  {
+    instance.nextInvalidReason = nextInvalidReason;
+  }
+}
+

--
Gitblit v1.10.0