From 16a4c18b4c101e8e3dc7b8be756de1807970065f Mon Sep 17 00:00:00 2001
From: neil_a_wilson <neil_a_wilson@localhost>
Date: Thu, 19 Jul 2007 16:12:32 +0000
Subject: [PATCH] Update the server to provide more complete support for the password policy control as described in draft-behera-ldap-password-policy.  In particular, improved support has been provided for all operations for the case in which a user must change his/her password before performing any other types of operations.  These changes also provide enhanced support for add and modify operations that are rejected because a password change is not acceptable for some reason.

---
 opends/src/server/org/opends/server/core/DirectoryServer.java                                           |   26 
 opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendWorkflowElement.java       |   84 ++
 opends/tests/unit-tests-testng/src/server/org/opends/server/admin/TestCfg.java                          |    2 
 opends/tests/unit-tests-testng/src/server/org/opends/server/controls/PasswordPolicyControlTestCase.java | 1530 +++++++++++++++++++++++++++++++++++++++++++++++++
 opends/src/server/org/opends/server/protocols/ldap/LDAPClientConnection.java                            |   96 ++
 opends/src/server/org/opends/server/tools/dsconfig/DSConfig.java                                        |   52 +
 opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendAddOperation.java          |   37 +
 7 files changed, 1,795 insertions(+), 32 deletions(-)

diff --git a/opends/src/server/org/opends/server/core/DirectoryServer.java b/opends/src/server/org/opends/server/core/DirectoryServer.java
index 6b7c19c..d1ec034 100644
--- a/opends/src/server/org/opends/server/core/DirectoryServer.java
+++ b/opends/src/server/org/opends/server/core/DirectoryServer.java
@@ -67,6 +67,8 @@
 import org.opends.server.config.ConfigEntry;
 import org.opends.server.config.ConfigException;
 import org.opends.server.config.JMXMBean;
+import org.opends.server.controls.PasswordPolicyErrorType;
+import org.opends.server.controls.PasswordPolicyResponseControl;
 import org.opends.server.extensions.ConfigFileHandler;
 import org.opends.server.extensions.JMXAlertHandler;
 import org.opends.server.loggers.TextErrorLogPublisher;
@@ -7203,6 +7205,18 @@
         case DELETE:
         case MODIFY_DN:
         case SEARCH:
+          // See if the request included the password policy request control.
+          // If it did, then add a corresponding response control.
+          for (Control c : operation.getRequestControls())
+          {
+            if (c.getOID().equals(OID_PASSWORD_POLICY_CONTROL))
+            {
+              operation.addResponseControl(new PasswordPolicyResponseControl(
+                   null, 0, PasswordPolicyErrorType.CHANGE_AFTER_RESET));
+              break;
+            }
+          }
+
           int    msgID   = MSGID_ENQUEUE_MUST_CHANGE_PASSWORD;
           String message = getMessage(msgID);
           throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message,
@@ -7217,6 +7231,18 @@
               ((! requestOID.equals(OID_PASSWORD_MODIFY_REQUEST)) &&
                (! requestOID.equals(OID_START_TLS_REQUEST))))
           {
+            // See if the request included the password policy request control.
+            // If it did, then add a corresponding response control.
+            for (Control c : operation.getRequestControls())
+            {
+              if (c.getOID().equals(OID_PASSWORD_POLICY_CONTROL))
+              {
+                operation.addResponseControl(new PasswordPolicyResponseControl(
+                     null, 0, PasswordPolicyErrorType.CHANGE_AFTER_RESET));
+                break;
+              }
+            }
+
             msgID   = MSGID_ENQUEUE_MUST_CHANGE_PASSWORD;
             message = getMessage(msgID);
             throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION,
diff --git a/opends/src/server/org/opends/server/protocols/ldap/LDAPClientConnection.java b/opends/src/server/org/opends/server/protocols/ldap/LDAPClientConnection.java
index 6226046..4cf5799 100644
--- a/opends/src/server/org/opends/server/protocols/ldap/LDAPClientConnection.java
+++ b/opends/src/server/org/opends/server/protocols/ldap/LDAPClientConnection.java
@@ -1898,8 +1898,18 @@
            new AddResponseProtocolOp(de.getResultCode().getIntValue(),
                                      de.getErrorMessage(), de.getMatchedDN(),
                                      de.getReferralURLs());
+
+      List<Control> responseControls = addOp.getResponseControls();
+      ArrayList<LDAPControl> responseLDAPControls =
+           new ArrayList<LDAPControl>(responseControls.size());
+      for (Control c : responseControls)
+      {
+        responseLDAPControls.add(new LDAPControl(c));
+      }
+
       sendLDAPMessage(securityProvider,
-                      new LDAPMessage(message.getMessageID(), responseOp));
+                      new LDAPMessage(message.getMessageID(), responseOp,
+                                      responseLDAPControls));
     }
 
 
@@ -2014,8 +2024,18 @@
            new BindResponseProtocolOp(de.getResultCode().getIntValue(),
                                       de.getErrorMessage(), de.getMatchedDN(),
                                       de.getReferralURLs());
+
+      List<Control> responseControls = bindOp.getResponseControls();
+      ArrayList<LDAPControl> responseLDAPControls =
+           new ArrayList<LDAPControl>(responseControls.size());
+      for (Control c : responseControls)
+      {
+        responseLDAPControls.add(new LDAPControl(c));
+      }
+
       sendLDAPMessage(securityProvider,
-                      new LDAPMessage(message.getMessageID(), responseOp));
+                      new LDAPMessage(message.getMessageID(), responseOp,
+                                      responseLDAPControls));
 
       // If it was a protocol error, then terminate the connection.
       if (de.getResultCode() == ResultCode.PROTOCOL_ERROR)
@@ -2086,8 +2106,18 @@
                                          de.getErrorMessage(),
                                          de.getMatchedDN(),
                                          de.getReferralURLs());
+
+      List<Control> responseControls = compareOp.getResponseControls();
+      ArrayList<LDAPControl> responseLDAPControls =
+           new ArrayList<LDAPControl>(responseControls.size());
+      for (Control c : responseControls)
+      {
+        responseLDAPControls.add(new LDAPControl(c));
+      }
+
       sendLDAPMessage(securityProvider,
-                      new LDAPMessage(message.getMessageID(), responseOp));
+                      new LDAPMessage(message.getMessageID(), responseOp,
+                                      responseLDAPControls));
     }
 
 
@@ -2147,8 +2177,18 @@
            new DeleteResponseProtocolOp(de.getResultCode().getIntValue(),
                                         de.getErrorMessage(), de.getMatchedDN(),
                                         de.getReferralURLs());
+
+      List<Control> responseControls = deleteOp.getResponseControls();
+      ArrayList<LDAPControl> responseLDAPControls =
+           new ArrayList<LDAPControl>(responseControls.size());
+      for (Control c : responseControls)
+      {
+        responseLDAPControls.add(new LDAPControl(c));
+      }
+
       sendLDAPMessage(securityProvider,
-                      new LDAPMessage(message.getMessageID(), responseOp));
+                      new LDAPMessage(message.getMessageID(), responseOp,
+                                      responseLDAPControls));
     }
 
 
@@ -2217,8 +2257,18 @@
                                           de.getErrorMessage(),
                                           de.getMatchedDN(),
                                           de.getReferralURLs());
+
+      List<Control> responseControls = extendedOp.getResponseControls();
+      ArrayList<LDAPControl> responseLDAPControls =
+           new ArrayList<LDAPControl>(responseControls.size());
+      for (Control c : responseControls)
+      {
+        responseLDAPControls.add(new LDAPControl(c));
+      }
+
       sendLDAPMessage(securityProvider,
-                      new LDAPMessage(message.getMessageID(), responseOp));
+                      new LDAPMessage(message.getMessageID(), responseOp,
+                                      responseLDAPControls));
     }
 
 
@@ -2278,8 +2328,18 @@
            new ModifyResponseProtocolOp(de.getResultCode().getIntValue(),
                                         de.getErrorMessage(), de.getMatchedDN(),
                                         de.getReferralURLs());
+
+      List<Control> responseControls = modifyOp.getResponseControls();
+      ArrayList<LDAPControl> responseLDAPControls =
+           new ArrayList<LDAPControl>(responseControls.size());
+      for (Control c : responseControls)
+      {
+        responseLDAPControls.add(new LDAPControl(c));
+      }
+
       sendLDAPMessage(securityProvider,
-                      new LDAPMessage(message.getMessageID(), responseOp));
+                      new LDAPMessage(message.getMessageID(), responseOp,
+                                      responseLDAPControls));
     }
 
 
@@ -2343,8 +2403,18 @@
                                           de.getErrorMessage(),
                                           de.getMatchedDN(),
                                           de.getReferralURLs());
+
+      List<Control> responseControls = modifyDNOp.getResponseControls();
+      ArrayList<LDAPControl> responseLDAPControls =
+           new ArrayList<LDAPControl>(responseControls.size());
+      for (Control c : responseControls)
+      {
+        responseLDAPControls.add(new LDAPControl(c));
+      }
+
       sendLDAPMessage(securityProvider,
-                      new LDAPMessage(message.getMessageID(), responseOp));
+                      new LDAPMessage(message.getMessageID(), responseOp,
+                                      responseLDAPControls));
     }
 
 
@@ -2410,8 +2480,18 @@
                                           de.getErrorMessage(),
                                           de.getMatchedDN(),
                                           de.getReferralURLs());
+
+      List<Control> responseControls = searchOp.getResponseControls();
+      ArrayList<LDAPControl> responseLDAPControls =
+           new ArrayList<LDAPControl>(responseControls.size());
+      for (Control c : responseControls)
+      {
+        responseLDAPControls.add(new LDAPControl(c));
+      }
+
       sendLDAPMessage(securityProvider,
-                      new LDAPMessage(message.getMessageID(), responseOp));
+                      new LDAPMessage(message.getMessageID(), responseOp,
+                                      responseLDAPControls));
     }
 
 
diff --git a/opends/src/server/org/opends/server/tools/dsconfig/DSConfig.java b/opends/src/server/org/opends/server/tools/dsconfig/DSConfig.java
index 21b4065..69ea295 100644
--- a/opends/src/server/org/opends/server/tools/dsconfig/DSConfig.java
+++ b/opends/src/server/org/opends/server/tools/dsconfig/DSConfig.java
@@ -94,25 +94,49 @@
    *          program.
    */
   public static void main(String[] args) {
-    DSConfig app = new DSConfig(System.in, System.out, System.err,
-        new LDAPManagementContextFactory());
-    // Only initialize the client environment when run as a standalone
-    // application.
-    try {
-      app.initializeClientEnvironment();
-    } catch (InitializationException e) {
-      // TODO: is this ok as an error message?
-      System.err.println(wrapText(e.getMessage(), MAX_LINE_WIDTH));
-      System.exit(1);
-    }
-
-    // Run the application.
-    int exitCode = app.run(args);
+    int exitCode = main(args, true, System.out, System.err);
     if (exitCode != 0) {
       System.exit(filterExitCode(exitCode));
     }
   }
 
+  /**
+   * Provides the command-line arguments to the main application for
+   * processing and returns the exit code as an integer.
+   *
+   * @param  args              The set of command-line arguments provided to
+   *                           this program.
+   * @param  initializeServer  Indicates whether to perform basic initialization
+   *                           (which should not be done if the tool is running
+   *                           in the same JVM as the server).
+   * @param  outStream         The output stream for standard output.
+   * @param  errStream         The output stream for standard error.
+   *
+   * @return  Zero to indicate that the program completed successfully, or
+   *          non-zero to indicate that an error occurred.
+   */
+  public static int main(String[] args, boolean initializeServer,
+                         OutputStream outStream, OutputStream errStream)
+  {
+    DSConfig app = new DSConfig(System.in, outStream, errStream,
+        new LDAPManagementContextFactory());
+    // Only initialize the client environment when run as a standalone
+    // application.
+    if (initializeServer)
+    {
+      try {
+        app.initializeClientEnvironment();
+      } catch (InitializationException e) {
+        // TODO: is this ok as an error message?
+        System.err.println(wrapText(e.getMessage(), MAX_LINE_WIDTH));
+        return 1;
+      }
+    }
+
+    // Run the application.
+    return app.run(args);
+  }
+
   // Flag indicating whether or not the application environment has
   // already been initialized.
   private boolean environmentInitialized = false;
diff --git a/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendAddOperation.java b/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendAddOperation.java
index 681aff6..211629a 100644
--- a/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendAddOperation.java
+++ b/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendAddOperation.java
@@ -29,6 +29,7 @@
 import static org.opends.server.config.ConfigConstants.*;
 import static org.opends.server.messages.CoreMessages.*;
 import static org.opends.server.messages.MessageHandler.getMessage;
+import static org.opends.server.util.ServerConstants.*;
 
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -39,6 +40,8 @@
 
 import org.opends.server.api.PasswordStorageScheme;
 import org.opends.server.api.PasswordValidator;
+import org.opends.server.controls.PasswordPolicyErrorType;
+import org.opends.server.controls.PasswordPolicyResponseControl;
 import org.opends.server.core.AddOperation;
 import org.opends.server.core.AddOperationWrapper;
 import org.opends.server.core.DirectoryServer;
@@ -51,6 +54,7 @@
 import org.opends.server.types.AttributeType;
 import org.opends.server.types.AttributeValue;
 import org.opends.server.types.ByteString;
+import org.opends.server.types.Control;
 import org.opends.server.types.DirectoryException;
 import org.opends.server.types.Entry;
 import org.opends.server.types.ObjectClass;
@@ -121,7 +125,7 @@
    *                              policy processing for the add operation.
    */
   public final void handlePasswordPolicy(PasswordPolicy passwordPolicy,
-                                          Entry userEntry)
+                                         Entry userEntry)
          throws DirectoryException
   {
     // See if a password was specified.
@@ -161,6 +165,8 @@
     if ((! passwordPolicy.allowMultiplePasswordValues()) && (values.size() > 1))
     {
       // FIXME -- What if they're pre-encoded and might all be the same?
+      addPWPolicyControl(PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED);
+
       int    msgID   = MSGID_PWPOLICY_MULTIPLE_PW_VALUES_NOT_ALLOWED;
       String message = getMessage(msgID, passwordAttribute.getNameOrOID());
       throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, message,
@@ -187,6 +193,9 @@
           }
           else
           {
+            addPWPolicyControl(
+                 PasswordPolicyErrorType.INSUFFICIENT_PASSWORD_QUALITY);
+
             int    msgID   = MSGID_PWPOLICY_PREENCODED_NOT_ALLOWED;
             String message = getMessage(msgID,
                                         passwordAttribute.getNameOrOID());
@@ -206,6 +215,9 @@
           }
           else
           {
+            addPWPolicyControl(
+                 PasswordPolicyErrorType.INSUFFICIENT_PASSWORD_QUALITY);
+
             int    msgID   = MSGID_PWPOLICY_PREENCODED_NOT_ALLOWED;
             String message = getMessage(msgID,
                                         passwordAttribute.getNameOrOID());
@@ -229,6 +241,9 @@
           if (! validator.passwordIsAcceptable(value, currentPasswords, this,
                                                userEntry, invalidReason))
           {
+            addPWPolicyControl(
+                 PasswordPolicyErrorType.INSUFFICIENT_PASSWORD_QUALITY);
+
             int    msgID   = MSGID_PWPOLICY_VALIDATION_FAILED;
             String message = getMessage(msgID, passwordAttribute.getNameOrOID(),
                                         String.valueOf(invalidReason));
@@ -289,6 +304,8 @@
     // If we should force change on add, then set the appropriate flag.
     if (passwordPolicy.forceChangeOnAdd())
     {
+      addPWPolicyControl(PasswordPolicyErrorType.CHANGE_AFTER_RESET);
+
       AttributeType resetType =
            DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_RESET_REQUIRED_LC);
       if (resetType == null)
@@ -309,6 +326,24 @@
   }
 
   /**
+   * Adds a password policy response control if the corresponding request
+   * control was included.
+   *
+   * @param  errorType  The error type to use for the response control.
+   */
+  private void addPWPolicyControl(PasswordPolicyErrorType errorType)
+  {
+    for (Control c : getRequestControls())
+    {
+      if (c.getOID().equals(OID_PASSWORD_POLICY_CONTROL))
+      {
+        addResponseControl(new PasswordPolicyResponseControl(null, 0,
+                                                             errorType));
+      }
+    }
+  }
+
+  /**
    * Adds the provided objectClass to the entry, along with its superior classes
    * if appropriate.
    *
diff --git a/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendWorkflowElement.java b/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendWorkflowElement.java
index d7470cf..ee3a804 100644
--- a/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendWorkflowElement.java
+++ b/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendWorkflowElement.java
@@ -227,6 +227,8 @@
 
     // Create a labeled block of code that we can break out of if a problem is
     // detected.
+    boolean                 pwPolicyControlRequested = false;
+    PasswordPolicyErrorType pwpErrorType             = null;
     modifyProcessing:
     {
       DN entryDN = localOp.getEntryDN();
@@ -261,8 +263,20 @@
         DN authzDN = localOp.getAuthorizationDN();
         if ((authzDN != null) && (! authzDN.equals(entryDN)))
         {
-          // The user will not be allowed to do anything else before
-          // the password gets changed.
+          // The user will not be allowed to do anything else before the
+          // password gets changed.  Also note that we haven't yet checked the
+          // request controls so we need to do that now to see if the password
+          // policy request control was provided.
+          for (Control c : localOp.getRequestControls())
+          {
+            if (c.getOID().equals(OID_PASSWORD_POLICY_CONTROL))
+            {
+              pwPolicyControlRequested = true;
+              pwpErrorType = PasswordPolicyErrorType.CHANGE_AFTER_RESET;
+              break;
+            }
+          }
+
           localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
 
           int msgID = MSGID_MODIFY_MUST_CHANGE_PASSWORD;
@@ -658,6 +672,10 @@
                 localOp.setProxiedAuthorizationDN(authorizationEntry.getDN());
               }
             }
+            else if (oid.equals(OID_PASSWORD_POLICY_CONTROL))
+            {
+              pwPolicyControlRequested = true;
+            }
 
             // NYI -- Add support for additional controls.
             else if (c.isCritical())
@@ -783,6 +801,9 @@
                     Privilege.PASSWORD_RESET,
                     localOp))
                 {
+                  pwpErrorType =
+                       PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED;
+
                   int msgID = MSGID_MODIFY_PWRESET_INSUFFICIENT_PRIVILEGES;
                   localOp.appendErrorMessage(getMessage(msgID));
                   localOp.setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
@@ -885,6 +906,8 @@
               if (selfChange &&
                   (! pwPolicyState.getPolicy().allowUserPasswordChanges()))
               {
+                pwpErrorType = PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED;
+
                 localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
 
                 int msgID = MSGID_MODIFY_NO_USER_PW_CHANGES;
@@ -898,6 +921,8 @@
               if (pwPolicyState.getPolicy().requireSecurePasswordChanges() &&
                   (! clientConnection.isSecure()))
               {
+                pwpErrorType = PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED;
+
                 localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
 
                 int msgID = MSGID_MODIFY_REQUIRE_SECURE_CHANGES;
@@ -910,6 +935,8 @@
               // previous change, then reject it.
               if (selfChange && pwPolicyState.isWithinMinimumAge())
               {
+                pwpErrorType = PasswordPolicyErrorType.PASSWORD_TOO_YOUNG;
+
                 localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
 
                 int msgID = MSGID_MODIFY_WITHIN_MINIMUM_AGE;
@@ -944,10 +971,12 @@
               // If there were multiple password values provided, then make
               // sure that's OK.
 
-              if (! localOp.isInternalOperation() &&
-                  ! pwPolicyState.getPolicy().allowExpiredPasswordChanges() &&
+              if ((! localOp.isInternalOperation()) &&
+                  (! pwPolicyState.getPolicy().allowMultiplePasswordValues()) &&
                   (passwordsToAdd > 1))
               {
+                pwpErrorType = PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED;
+
                 localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
 
                 int msgID = MSGID_MODIFY_MULTIPLE_VALUES_NOT_ALLOWED;
@@ -966,6 +995,9 @@
                   if ((!localOp.isInternalOperation()) &&
                       ! pwPolicyState.getPolicy().allowPreEncodedPasswords())
                   {
+                    pwpErrorType =
+                         PasswordPolicyErrorType.INSUFFICIENT_PASSWORD_QUALITY;
+
                     localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
 
                     int msgID = MSGID_MODIFY_NO_PREENCODED_PASSWORDS;
@@ -985,6 +1017,9 @@
                     // exist.
                     if (pwPolicyState.passwordMatches(v.getValue()))
                     {
+                      pwpErrorType =
+                           PasswordPolicyErrorType.PASSWORD_IN_HISTORY;
+
                       localOp.setResultCode(
                           ResultCode.ATTRIBUTE_OR_VALUE_EXISTS);
 
@@ -1044,6 +1079,9 @@
                 {
                   if ((!localOp.isInternalOperation()) && selfChange)
                   {
+                    pwpErrorType =
+                         PasswordPolicyErrorType.INSUFFICIENT_PASSWORD_QUALITY;
+
                     localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
 
                     int msgID = MSGID_MODIFY_NO_PREENCODED_PASSWORDS;
@@ -1774,6 +1812,8 @@
               pwPolicyState.getPolicy().requireCurrentPassword() &&
               (! currentPasswordProvided))
           {
+            pwpErrorType = PasswordPolicyErrorType.MUST_SUPPLY_OLD_PASSWORD;
+
             localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
 
             int msgID = MSGID_MODIFY_PW_CHANGE_REQUIRES_CURRENT_PW;
@@ -1787,6 +1827,8 @@
           if ((numPasswords > 1) &&
               (! pwPolicyState.getPolicy().allowMultiplePasswordValues()))
           {
+            pwpErrorType = PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED;
+
             localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
 
             int msgID = MSGID_MODIFY_MULTIPLE_PASSWORDS_NOT_ALLOWED;
@@ -1856,6 +1898,9 @@
                     clearPasswords,
                     invalidReason))
                 {
+                  pwpErrorType =
+                       PasswordPolicyErrorType.INSUFFICIENT_PASSWORD_QUALITY;
+
                   localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
 
                   int msgID = MSGID_MODIFY_PW_VALIDATION_FAILED;
@@ -1881,6 +1926,8 @@
                   if (selfChange || (! pwPolicyState.getPolicy().
                                             skipValidationForAdministrators()))
                   {
+                    pwpErrorType = PasswordPolicyErrorType.PASSWORD_IN_HISTORY;
+
                     localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
 
                     int msgID = MSGID_MODIFY_PW_IN_HISTORY;
@@ -1924,8 +1971,8 @@
         {
           // See if the account was locked for any reason.
           wasLocked = pwPolicyState.lockedDueToIdleInterval() ||
-          pwPolicyState.lockedDueToMaximumResetAge() ||
-          pwPolicyState.lockedDueToFailures();
+                      pwPolicyState.lockedDueToMaximumResetAge() ||
+                      pwPolicyState.lockedDueToFailures();
 
           // Update the password policy state attributes in the user's entry.
           // If the modification fails, then these changes won't be applied.
@@ -1943,6 +1990,12 @@
             }
             else
             {
+              if ((pwpErrorType == null) &&
+                  pwPolicyState.getPolicy().forceChangeOnReset())
+              {
+                pwpErrorType = PasswordPolicyErrorType.CHANGE_AFTER_RESET;
+              }
+
               pwPolicyState.setMustChangePassword(
                    pwPolicyState.getPolicy().forceChangeOnReset());
             }
@@ -1971,6 +2024,8 @@
         {
             // The user will not be allowed to do anything else before
             // the password gets changed.
+            pwpErrorType = PasswordPolicyErrorType.CHANGE_AFTER_RESET;
+
             localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
 
             int msgID = MSGID_MODIFY_MUST_CHANGE_PASSWORD;
@@ -2378,6 +2433,15 @@
     }
 
 
+    // If the password policy request control was included, then make sure we
+    // send the corresponding response control.
+    if (pwPolicyControlRequested)
+    {
+      localOp.addResponseControl(
+           new PasswordPolicyResponseControl(null, 0, pwpErrorType));
+    }
+
+
     // Indicate that it is now too late to attempt to cancel the operation.
     localOp.setCancelResult(CancelResult.TOO_LATE);
 
@@ -5222,8 +5286,7 @@
           }
         }
 
-        // Check to see if there are any controls in the request. If so,
-        // then
+        // Check to see if there are any controls in the request. If so, then
         // see if there is any special processing required.
         boolean                    noOp            = false;
         LDAPPostReadRequestControl postReadRequest = null;
@@ -5487,6 +5550,11 @@
                 localOp.setProxiedAuthorizationDN(authorizationEntry.getDN());
               }
             }
+            else if (oid.equals(OID_PASSWORD_POLICY_CONTROL))
+            {
+              // We don't need to do anything here because it's already handled
+              // in LocalBackendAddOperation.handlePasswordPolicy().
+            }
 
             // NYI -- Add support for additional controls.
             else if (c.isCritical())
diff --git a/opends/tests/unit-tests-testng/src/server/org/opends/server/admin/TestCfg.java b/opends/tests/unit-tests-testng/src/server/org/opends/server/admin/TestCfg.java
index 86db1df..d1daf3d 100644
--- a/opends/tests/unit-tests-testng/src/server/org/opends/server/admin/TestCfg.java
+++ b/opends/tests/unit-tests-testng/src/server/org/opends/server/admin/TestCfg.java
@@ -73,7 +73,7 @@
     RootCfgDefn.getInstance().deregisterRelationDefinition(
         RD_TEST_ONE_TO_MANY_PARENT);
     RootCfgDefn.getInstance().deregisterRelationDefinition(
-        RD_TEST_ONE_TO_MANY_PARENT);
+        RD_TEST_ONE_TO_ZERO_OR_ONE_PARENT);
   }
 
 
diff --git a/opends/tests/unit-tests-testng/src/server/org/opends/server/controls/PasswordPolicyControlTestCase.java b/opends/tests/unit-tests-testng/src/server/org/opends/server/controls/PasswordPolicyControlTestCase.java
new file mode 100644
index 0000000..f75804b
--- /dev/null
+++ b/opends/tests/unit-tests-testng/src/server/org/opends/server/controls/PasswordPolicyControlTestCase.java
@@ -0,0 +1,1530 @@
+/*
+ * 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 2007 Sun Microsystems, Inc.
+ */
+package org.opends.server.controls;
+
+
+
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import org.opends.server.TestCaseUtils;
+import org.opends.server.protocols.asn1.*;
+import org.opends.server.protocols.ldap.*;
+import org.opends.server.types.*;
+import org.opends.server.tools.dsconfig.DSConfig;
+
+import static org.testng.Assert.*;
+
+import static org.opends.server.util.ServerConstants.*;
+
+
+
+/**
+ * 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);
+
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "cn=Directory Manager",
+      "-w", "password",
+      "set-password-policy-prop",
+      "--policy-name", "Default Password Policy",
+      "--set", "force-change-on-add:true"
+    };
+    assertEquals(DSConfig.main(args, false, System.out, System.err), 0);
+
+    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());
+    ASN1Reader r = new ASN1Reader(s);
+    ASN1Writer w = new ASN1Writer(s);
+
+    try
+    {
+      ArrayList<LDAPControl> controls = new ArrayList<LDAPControl>();
+      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));
+
+      BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(
+           new ASN1OctetString("uid=test.user,o=test"), 3,
+           new ASN1OctetString("password"));
+      LDAPMessage message = new LDAPMessage(1, bindRequest, controls);
+      w.writeElement(message.encode());
+
+      message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+      BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
+      assertEquals(bindResponse.getResultCode(), LDAPResultCode.SUCCESS);
+
+      controls = message.getControls();
+      assertNotNull(controls);
+      assertFalse(controls.isEmpty());
+
+      boolean found = false;
+      for (LDAPControl c : controls)
+      {
+        if (c.getOID().equals(OID_PASSWORD_POLICY_CONTROL))
+        {
+          PasswordPolicyResponseControl pwpControl =
+               PasswordPolicyResponseControl.decodeControl(c.getControl());
+          assertEquals(pwpControl.getErrorType(),
+                       PasswordPolicyErrorType.CHANGE_AFTER_RESET);
+          found = true;
+        }
+      }
+      assertTrue(found);
+
+
+      ArrayList<RawAttribute> rawAttrs = new ArrayList<RawAttribute>();
+      rawAttrs.add(RawAttribute.create("objectClass", "organizationalUnit"));
+      rawAttrs.add(RawAttribute.create("ou", "People"));
+
+      AddRequestProtocolOp addRequest = new AddRequestProtocolOp(
+           new ASN1OctetString("ou=People,o=test"), rawAttrs);
+
+      controls = new ArrayList<LDAPControl>();
+      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));
+
+      message = new LDAPMessage(2, addRequest, controls);
+      w.writeElement(message.encode());
+
+      message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+      AddResponseProtocolOp addResponse = message.getAddResponseProtocolOp();
+      assertFalse(addResponse.getResultCode() == LDAPResultCode.SUCCESS);
+
+      controls = message.getControls();
+      assertNotNull(controls);
+      assertFalse(controls.isEmpty());
+
+      found = false;
+      for (LDAPControl c : controls)
+      {
+        if (c.getOID().equals(OID_PASSWORD_POLICY_CONTROL))
+        {
+          PasswordPolicyResponseControl pwpControl =
+               PasswordPolicyResponseControl.decodeControl(c.getControl());
+          assertEquals(pwpControl.getErrorType(),
+                       PasswordPolicyErrorType.CHANGE_AFTER_RESET);
+          found = true;
+        }
+      }
+      assertTrue(found);
+    }
+    finally
+    {
+      args = new String[]
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+        "-D", "cn=Directory Manager",
+        "-w", "password",
+        "set-password-policy-prop",
+        "--policy-name", "Default Password Policy",
+        "--set", "force-change-on-add:false"
+      };
+      assertEquals(DSConfig.main(args, false, System.out, System.err), 0);
+
+      try
+      {
+        s.close();
+      } catch (Exception e) {}
+    }
+  }
+
+
+
+  /**
+   * 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());
+    ASN1Reader r = new ASN1Reader(s);
+    ASN1Writer w = new ASN1Writer(s);
+
+    try
+    {
+      BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(
+           new ASN1OctetString("cn=Directory Manager"), 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(), LDAPResultCode.SUCCESS);
+
+
+      ArrayList<RawAttribute> rawAttrs = new ArrayList<RawAttribute>();
+      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(
+           new ASN1OctetString("ou=uid=test.user,o=test"), rawAttrs);
+
+      ArrayList<LDAPControl> controls = new ArrayList<LDAPControl>();
+      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));
+
+      message = new LDAPMessage(2, addRequest, controls);
+      w.writeElement(message.encode());
+
+      message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+      AddResponseProtocolOp addResponse = message.getAddResponseProtocolOp();
+      assertFalse(addResponse.getResultCode() == LDAPResultCode.SUCCESS);
+
+      controls = message.getControls();
+      assertNotNull(controls);
+      assertFalse(controls.isEmpty());
+
+      boolean found = false;
+      for (LDAPControl c : controls)
+      {
+        if (c.getOID().equals(OID_PASSWORD_POLICY_CONTROL))
+        {
+          PasswordPolicyResponseControl pwpControl =
+               PasswordPolicyResponseControl.decodeControl(c.getControl());
+          assertEquals(pwpControl.getErrorType(),
+                       PasswordPolicyErrorType.INSUFFICIENT_PASSWORD_QUALITY);
+          found = true;
+        }
+      }
+      assertTrue(found);
+    }
+    finally
+    {
+      try
+      {
+        s.close();
+      } catch (Exception e) {}
+    }
+  }
+
+
+
+  /**
+   * 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);
+
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "cn=Directory Manager",
+      "-w", "password",
+      "set-password-policy-prop",
+      "--policy-name", "Default Password Policy",
+      "--add", "password-validator-dn:cn=Length-Based Password Validator," +
+           "cn=Password Validators,cn=config"
+    };
+    assertEquals(DSConfig.main(args, false, System.out, System.err), 0);
+
+    Socket s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort());
+    ASN1Reader r = new ASN1Reader(s);
+    ASN1Writer w = new ASN1Writer(s);
+
+    try
+    {
+      BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(
+           new ASN1OctetString("cn=Directory Manager"), 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(), LDAPResultCode.SUCCESS);
+
+
+      ArrayList<RawAttribute> rawAttrs = new ArrayList<RawAttribute>();
+      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(
+           new ASN1OctetString("ou=uid=test.user,o=test"), rawAttrs);
+
+      ArrayList<LDAPControl> controls = new ArrayList<LDAPControl>();
+      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));
+
+      message = new LDAPMessage(2, addRequest, controls);
+      w.writeElement(message.encode());
+
+      message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+      AddResponseProtocolOp addResponse = message.getAddResponseProtocolOp();
+      assertFalse(addResponse.getResultCode() == LDAPResultCode.SUCCESS);
+
+      controls = message.getControls();
+      assertNotNull(controls);
+      assertFalse(controls.isEmpty());
+
+      boolean found = false;
+      for (LDAPControl c : controls)
+      {
+        if (c.getOID().equals(OID_PASSWORD_POLICY_CONTROL))
+        {
+          PasswordPolicyResponseControl pwpControl =
+               PasswordPolicyResponseControl.decodeControl(c.getControl());
+          assertEquals(pwpControl.getErrorType(),
+                       PasswordPolicyErrorType.INSUFFICIENT_PASSWORD_QUALITY);
+          found = true;
+        }
+      }
+      assertTrue(found);
+    }
+    finally
+    {
+      args = new String[]
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+        "-D", "cn=Directory Manager",
+        "-w", "password",
+        "set-password-policy-prop",
+        "--policy-name", "Default Password Policy",
+        "--remove", "password-validator-dn:cn=Length-Based Password " +
+             "Validator,cn=Password Validators,cn=config"
+      };
+      assertEquals(DSConfig.main(args, false, System.out, System.err), 0);
+
+      try
+      {
+        s.close();
+      } catch (Exception e) {}
+    }
+  }
+
+
+
+  /**
+   * 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);
+
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "cn=Directory Manager",
+      "-w", "password",
+      "set-password-policy-prop",
+      "--policy-name", "Default Password Policy",
+      "--set", "lockout-failure-count:3"
+    };
+    assertEquals(DSConfig.main(args, false, System.out, System.err), 0);
+
+    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());
+    ASN1Reader r = new ASN1Reader(s);
+    ASN1Writer w = new ASN1Writer(s);
+
+    try
+    {
+      BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(
+           new ASN1OctetString("uid=test.user,o=test"), 3,
+           new ASN1OctetString("wrong"));
+
+      for (int i=1; i <= 3; i++)
+      {
+        LDAPMessage message = new LDAPMessage(1, bindRequest);
+        w.writeElement(message.encode());
+
+        message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+        BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
+        assertFalse(bindResponse.getResultCode() == LDAPResultCode.SUCCESS);
+      }
+
+      bindRequest = new BindRequestProtocolOp(
+           new ASN1OctetString("uid=test.user,o=test"), 3,
+           new ASN1OctetString("password"));
+
+      ArrayList<LDAPControl> controls = new ArrayList<LDAPControl>();
+      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));
+
+      LDAPMessage message = new LDAPMessage(4, bindRequest, controls);
+      w.writeElement(message.encode());
+
+      message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+      BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
+      assertFalse(bindResponse.getResultCode() == LDAPResultCode.SUCCESS);
+
+      controls = message.getControls();
+      assertNotNull(controls);
+      assertFalse(controls.isEmpty());
+
+      boolean found = false;
+      for (LDAPControl c : controls)
+      {
+        if (c.getOID().equals(OID_PASSWORD_POLICY_CONTROL))
+        {
+          PasswordPolicyResponseControl pwpControl =
+               PasswordPolicyResponseControl.decodeControl(c.getControl());
+          assertEquals(pwpControl.getErrorType(),
+                       PasswordPolicyErrorType.ACCOUNT_LOCKED);
+          found = true;
+        }
+      }
+      assertTrue(found);
+    }
+    finally
+    {
+      args = new String[]
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+        "-D", "cn=Directory Manager",
+        "-w", "password",
+        "set-password-policy-prop",
+        "--policy-name", "Default Password Policy",
+        "--set", "lockout-failure-count:0"
+      };
+      assertEquals(DSConfig.main(args, false, System.out, System.err), 0);
+
+      try
+      {
+        s.close();
+      } catch (Exception e) {}
+    }
+  }
+
+
+
+  /**
+   * 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);
+
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "cn=Directory Manager",
+      "-w", "password",
+      "set-password-policy-prop",
+      "--policy-name", "Default Password Policy",
+      "--set", "force-change-on-add:true"
+    };
+    assertEquals(DSConfig.main(args, false, System.out, System.err), 0);
+
+    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());
+    ASN1Reader r = new ASN1Reader(s);
+    ASN1Writer w = new ASN1Writer(s);
+
+    try
+    {
+      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(), LDAPResultCode.SUCCESS);
+
+
+      CompareRequestProtocolOp compareRequest =
+           new CompareRequestProtocolOp(new ASN1OctetString("o=test"), "o",
+                                        new ASN1OctetString("test"));
+
+      ArrayList<LDAPControl> controls = new ArrayList<LDAPControl>();
+      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));
+
+      message = new LDAPMessage(2, compareRequest, controls);
+      w.writeElement(message.encode());
+
+      message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+      CompareResponseProtocolOp compareResponse =
+           message.getCompareResponseProtocolOp();
+      assertFalse(compareResponse.getResultCode() == LDAPResultCode.SUCCESS);
+
+      controls = message.getControls();
+      assertNotNull(controls);
+      assertFalse(controls.isEmpty());
+
+      boolean found = false;
+      for (LDAPControl c : controls)
+      {
+        if (c.getOID().equals(OID_PASSWORD_POLICY_CONTROL))
+        {
+          PasswordPolicyResponseControl pwpControl =
+               PasswordPolicyResponseControl.decodeControl(c.getControl());
+          assertEquals(pwpControl.getErrorType(),
+                       PasswordPolicyErrorType.CHANGE_AFTER_RESET);
+          found = true;
+        }
+      }
+      assertTrue(found);
+    }
+    finally
+    {
+      args = new String[]
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+        "-D", "cn=Directory Manager",
+        "-w", "password",
+        "set-password-policy-prop",
+        "--policy-name", "Default Password Policy",
+        "--set", "force-change-on-add:false"
+      };
+      assertEquals(DSConfig.main(args, false, System.out, System.err), 0);
+
+      try
+      {
+        s.close();
+      } catch (Exception e) {}
+    }
+  }
+
+
+
+  /**
+   * 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);
+
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "cn=Directory Manager",
+      "-w", "password",
+      "set-password-policy-prop",
+      "--policy-name", "Default Password Policy",
+      "--set", "force-change-on-add:true"
+    };
+    assertEquals(DSConfig.main(args, false, System.out, System.err), 0);
+
+    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());
+    ASN1Reader r = new ASN1Reader(s);
+    ASN1Writer w = new ASN1Writer(s);
+
+    try
+    {
+      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(), LDAPResultCode.SUCCESS);
+
+
+      DeleteRequestProtocolOp deleteRequest =
+           new DeleteRequestProtocolOp(new ASN1OctetString("ou=People,o=test"));
+
+      ArrayList<LDAPControl> controls = new ArrayList<LDAPControl>();
+      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));
+
+      message = new LDAPMessage(2, deleteRequest, controls);
+      w.writeElement(message.encode());
+
+      message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+      DeleteResponseProtocolOp deleteResponse =
+           message.getDeleteResponseProtocolOp();
+      assertFalse(deleteResponse.getResultCode() == LDAPResultCode.SUCCESS);
+
+      controls = message.getControls();
+      assertNotNull(controls);
+      assertFalse(controls.isEmpty());
+
+      boolean found = false;
+      for (LDAPControl c : controls)
+      {
+        if (c.getOID().equals(OID_PASSWORD_POLICY_CONTROL))
+        {
+          PasswordPolicyResponseControl pwpControl =
+               PasswordPolicyResponseControl.decodeControl(c.getControl());
+          assertEquals(pwpControl.getErrorType(),
+                       PasswordPolicyErrorType.CHANGE_AFTER_RESET);
+          found = true;
+        }
+      }
+      assertTrue(found);
+    }
+    finally
+    {
+      args = new String[]
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+        "-D", "cn=Directory Manager",
+        "-w", "password",
+        "set-password-policy-prop",
+        "--policy-name", "Default Password Policy",
+        "--set", "force-change-on-add:false"
+      };
+      assertEquals(DSConfig.main(args, false, System.out, System.err), 0);
+
+      try
+      {
+        s.close();
+      } catch (Exception e) {}
+    }
+  }
+
+
+
+  /**
+   * 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.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testModifyMustChange()
+         throws Exception
+  {
+    TestCaseUtils.initializeTestBackend(true);
+
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "cn=Directory Manager",
+      "-w", "password",
+      "set-password-policy-prop",
+      "--policy-name", "Default Password Policy",
+      "--set", "force-change-on-add:true"
+    };
+    assertEquals(DSConfig.main(args, false, System.out, System.err), 0);
+
+    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());
+    ASN1Reader r = new ASN1Reader(s);
+    ASN1Writer w = new ASN1Writer(s);
+
+    try
+    {
+      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(), LDAPResultCode.SUCCESS);
+
+
+      ArrayList<RawModification> mods = new ArrayList<RawModification>();
+      mods.add(RawModification.create(ModificationType.REPLACE, "description",
+                                      "foo"));
+
+      ModifyRequestProtocolOp modifyRequest =
+           new ModifyRequestProtocolOp(new ASN1OctetString("o=test"), mods);
+
+      ArrayList<LDAPControl> controls = new ArrayList<LDAPControl>();
+      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));
+
+      message = new LDAPMessage(2, modifyRequest, controls);
+      w.writeElement(message.encode());
+
+      message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+      ModifyResponseProtocolOp modifyResponse =
+           message.getModifyResponseProtocolOp();
+      assertFalse(modifyResponse.getResultCode() == LDAPResultCode.SUCCESS);
+
+      controls = message.getControls();
+      assertNotNull(controls);
+      assertFalse(controls.isEmpty());
+
+      boolean found = false;
+      for (LDAPControl c : controls)
+      {
+        if (c.getOID().equals(OID_PASSWORD_POLICY_CONTROL))
+        {
+          PasswordPolicyResponseControl pwpControl =
+               PasswordPolicyResponseControl.decodeControl(c.getControl());
+          assertEquals(pwpControl.getErrorType(),
+                       PasswordPolicyErrorType.CHANGE_AFTER_RESET);
+          found = true;
+        }
+      }
+      assertTrue(found);
+    }
+    finally
+    {
+      args = new String[]
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+        "-D", "cn=Directory Manager",
+        "-w", "password",
+        "set-password-policy-prop",
+        "--policy-name", "Default Password Policy",
+        "--set", "force-change-on-add:false"
+      };
+      assertEquals(DSConfig.main(args, false, System.out, System.err), 0);
+
+      try
+      {
+        s.close();
+      } catch (Exception e) {}
+    }
+  }
+
+
+
+  /**
+   * 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);
+
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "cn=Directory Manager",
+      "-w", "password",
+      "set-password-policy-prop",
+      "--policy-name", "Default Password Policy",
+      "--set", "allow-user-password-changes:false"
+    };
+    assertEquals(DSConfig.main(args, false, System.out, System.err), 0);
+
+    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());
+    ASN1Reader r = new ASN1Reader(s);
+    ASN1Writer w = new ASN1Writer(s);
+
+    try
+    {
+      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(), LDAPResultCode.SUCCESS);
+
+
+      ArrayList<RawModification> mods = new ArrayList<RawModification>();
+      mods.add(RawModification.create(ModificationType.REPLACE, "userPassword",
+                                      "newpassword"));
+
+      ModifyRequestProtocolOp modifyRequest =
+           new ModifyRequestProtocolOp(
+                    new ASN1OctetString("uid=test.user,o=test"), mods);
+
+      ArrayList<LDAPControl> controls = new ArrayList<LDAPControl>();
+      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));
+
+      message = new LDAPMessage(2, modifyRequest, controls);
+      w.writeElement(message.encode());
+
+      message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+      ModifyResponseProtocolOp modifyResponse =
+           message.getModifyResponseProtocolOp();
+      assertFalse(modifyResponse.getResultCode() == LDAPResultCode.SUCCESS);
+
+      controls = message.getControls();
+      assertNotNull(controls);
+      assertFalse(controls.isEmpty());
+
+      boolean found = false;
+      for (LDAPControl c : controls)
+      {
+        if (c.getOID().equals(OID_PASSWORD_POLICY_CONTROL))
+        {
+          PasswordPolicyResponseControl pwpControl =
+               PasswordPolicyResponseControl.decodeControl(c.getControl());
+          assertEquals(pwpControl.getErrorType(),
+                       PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED);
+          found = true;
+        }
+      }
+      assertTrue(found);
+    }
+    finally
+    {
+      args = new String[]
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+        "-D", "cn=Directory Manager",
+        "-w", "password",
+        "set-password-policy-prop",
+        "--policy-name", "Default Password Policy",
+        "--set", "allow-user-password-changes:true"
+      };
+      assertEquals(DSConfig.main(args, false, System.out, System.err), 0);
+
+      try
+      {
+        s.close();
+      } catch (Exception e) {}
+    }
+  }
+
+
+
+  /**
+   * 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);
+
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "cn=Directory Manager",
+      "-w", "password",
+      "set-password-policy-prop",
+      "--policy-name", "Default Password Policy",
+      "--set", "password-history-count:5"
+    };
+    assertEquals(DSConfig.main(args, false, System.out, System.err), 0);
+
+    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());
+    ASN1Reader r = new ASN1Reader(s);
+    ASN1Writer w = new ASN1Writer(s);
+
+    try
+    {
+      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(), LDAPResultCode.SUCCESS);
+
+
+      ArrayList<RawModification> mods = new ArrayList<RawModification>();
+      mods.add(RawModification.create(ModificationType.REPLACE, "userPassword",
+                                      "password"));
+
+      ModifyRequestProtocolOp modifyRequest =
+           new ModifyRequestProtocolOp(
+                    new ASN1OctetString("uid=test.user,o=test"), mods);
+
+      ArrayList<LDAPControl> controls = new ArrayList<LDAPControl>();
+      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));
+
+      message = new LDAPMessage(2, modifyRequest, controls);
+      w.writeElement(message.encode());
+
+      message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+      ModifyResponseProtocolOp modifyResponse =
+           message.getModifyResponseProtocolOp();
+      assertFalse(modifyResponse.getResultCode() == LDAPResultCode.SUCCESS);
+
+      controls = message.getControls();
+      assertNotNull(controls);
+      assertFalse(controls.isEmpty());
+
+      boolean found = false;
+      for (LDAPControl c : controls)
+      {
+        if (c.getOID().equals(OID_PASSWORD_POLICY_CONTROL))
+        {
+          PasswordPolicyResponseControl pwpControl =
+               PasswordPolicyResponseControl.decodeControl(c.getControl());
+          assertEquals(pwpControl.getErrorType(),
+                       PasswordPolicyErrorType.PASSWORD_IN_HISTORY);
+          found = true;
+        }
+      }
+      assertTrue(found);
+    }
+    finally
+    {
+      args = new String[]
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+        "-D", "cn=Directory Manager",
+        "-w", "password",
+        "set-password-policy-prop",
+        "--policy-name", "Default Password Policy",
+        "--set", "password-history-count:0"
+      };
+      assertEquals(DSConfig.main(args, false, System.out, System.err), 0);
+
+      try
+      {
+        s.close();
+      } catch (Exception e) {}
+    }
+  }
+
+
+
+  /**
+   * 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);
+
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "cn=Directory Manager",
+      "-w", "password",
+      "set-password-policy-prop",
+      "--policy-name", "Default Password Policy",
+      "--set", "password-change-requires-current-password:true"
+    };
+    assertEquals(DSConfig.main(args, false, System.out, System.err), 0);
+
+    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());
+    ASN1Reader r = new ASN1Reader(s);
+    ASN1Writer w = new ASN1Writer(s);
+
+    try
+    {
+      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(), LDAPResultCode.SUCCESS);
+
+
+      ArrayList<RawModification> mods = new ArrayList<RawModification>();
+      mods.add(RawModification.create(ModificationType.REPLACE, "userPassword",
+                                      "newpassword"));
+
+      ModifyRequestProtocolOp modifyRequest =
+           new ModifyRequestProtocolOp(
+                    new ASN1OctetString("uid=test.user,o=test"), mods);
+
+      ArrayList<LDAPControl> controls = new ArrayList<LDAPControl>();
+      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));
+
+      message = new LDAPMessage(2, modifyRequest, controls);
+      w.writeElement(message.encode());
+
+      message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+      ModifyResponseProtocolOp modifyResponse =
+           message.getModifyResponseProtocolOp();
+      assertFalse(modifyResponse.getResultCode() == LDAPResultCode.SUCCESS);
+
+      controls = message.getControls();
+      assertNotNull(controls);
+      assertFalse(controls.isEmpty());
+
+      boolean found = false;
+      for (LDAPControl c : controls)
+      {
+        if (c.getOID().equals(OID_PASSWORD_POLICY_CONTROL))
+        {
+          PasswordPolicyResponseControl pwpControl =
+               PasswordPolicyResponseControl.decodeControl(c.getControl());
+          assertEquals(pwpControl.getErrorType(),
+                       PasswordPolicyErrorType.MUST_SUPPLY_OLD_PASSWORD);
+          found = true;
+        }
+      }
+      assertTrue(found);
+    }
+    finally
+    {
+      args = new String[]
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+        "-D", "cn=Directory Manager",
+        "-w", "password",
+        "set-password-policy-prop",
+        "--policy-name", "Default Password Policy",
+        "--set", "password-change-requires-current-password:false"
+      };
+      assertEquals(DSConfig.main(args, false, System.out, System.err), 0);
+
+      try
+      {
+        s.close();
+      } catch (Exception e) {}
+    }
+  }
+
+
+
+  /**
+   * 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);
+
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "cn=Directory Manager",
+      "-w", "password",
+      "set-password-policy-prop",
+      "--policy-name", "Default Password Policy",
+      "--set", "minimum-password-age:24 hours"
+    };
+    assertEquals(DSConfig.main(args, false, System.out, System.err), 0);
+
+    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());
+    ASN1Reader r = new ASN1Reader(s);
+    ASN1Writer w = new ASN1Writer(s);
+
+    try
+    {
+      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(), LDAPResultCode.SUCCESS);
+
+
+      ArrayList<RawModification> mods = new ArrayList<RawModification>();
+      mods.add(RawModification.create(ModificationType.REPLACE, "userPassword",
+                                      "newpassword"));
+
+      ModifyRequestProtocolOp modifyRequest =
+           new ModifyRequestProtocolOp(
+                    new ASN1OctetString("uid=test.user,o=test"), mods);
+
+      ArrayList<LDAPControl> controls = new ArrayList<LDAPControl>();
+      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));
+
+      message = new LDAPMessage(2, modifyRequest, controls);
+      w.writeElement(message.encode());
+
+      message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+      ModifyResponseProtocolOp modifyResponse =
+           message.getModifyResponseProtocolOp();
+      assertFalse(modifyResponse.getResultCode() == LDAPResultCode.SUCCESS);
+
+      controls = message.getControls();
+      assertNotNull(controls);
+      assertFalse(controls.isEmpty());
+
+      boolean found = false;
+      for (LDAPControl c : controls)
+      {
+        if (c.getOID().equals(OID_PASSWORD_POLICY_CONTROL))
+        {
+          PasswordPolicyResponseControl pwpControl =
+               PasswordPolicyResponseControl.decodeControl(c.getControl());
+          assertEquals(pwpControl.getErrorType(),
+                       PasswordPolicyErrorType.PASSWORD_TOO_YOUNG);
+          found = true;
+        }
+      }
+      assertTrue(found);
+    }
+    finally
+    {
+      args = new String[]
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+        "-D", "cn=Directory Manager",
+        "-w", "password",
+        "set-password-policy-prop",
+        "--policy-name", "Default Password Policy",
+        "--set", "minimum-password-age:0 seconds"
+      };
+      assertEquals(DSConfig.main(args, false, System.out, System.err), 0);
+
+      try
+      {
+        s.close();
+      } catch (Exception e) {}
+    }
+  }
+
+
+
+  /**
+   * 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);
+
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "cn=Directory Manager",
+      "-w", "password",
+      "set-password-policy-prop",
+      "--policy-name", "Default Password Policy",
+      "--set", "force-change-on-add:true"
+    };
+    assertEquals(DSConfig.main(args, false, System.out, System.err), 0);
+
+    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());
+    ASN1Reader r = new ASN1Reader(s);
+    ASN1Writer w = new ASN1Writer(s);
+
+    try
+    {
+      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(), LDAPResultCode.SUCCESS);
+
+
+      ModifyDNRequestProtocolOp modifyDNRequest =
+           new ModifyDNRequestProtocolOp(
+                    new ASN1OctetString("ou=People,o=test"),
+                    new ASN1OctetString("ou=Users"), true);
+
+      ArrayList<LDAPControl> controls = new ArrayList<LDAPControl>();
+      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));
+
+      message = new LDAPMessage(2, modifyDNRequest, controls);
+      w.writeElement(message.encode());
+
+      message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+      ModifyDNResponseProtocolOp modifyDNResponse =
+           message.getModifyDNResponseProtocolOp();
+      assertFalse(modifyDNResponse.getResultCode() == LDAPResultCode.SUCCESS);
+
+      controls = message.getControls();
+      assertNotNull(controls);
+      assertFalse(controls.isEmpty());
+
+      boolean found = false;
+      for (LDAPControl c : controls)
+      {
+        if (c.getOID().equals(OID_PASSWORD_POLICY_CONTROL))
+        {
+          PasswordPolicyResponseControl pwpControl =
+               PasswordPolicyResponseControl.decodeControl(c.getControl());
+          assertEquals(pwpControl.getErrorType(),
+                       PasswordPolicyErrorType.CHANGE_AFTER_RESET);
+          found = true;
+        }
+      }
+      assertTrue(found);
+    }
+    finally
+    {
+      args = new String[]
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+        "-D", "cn=Directory Manager",
+        "-w", "password",
+        "set-password-policy-prop",
+        "--policy-name", "Default Password Policy",
+        "--set", "force-change-on-add:false"
+      };
+      assertEquals(DSConfig.main(args, false, System.out, System.err), 0);
+
+      try
+      {
+        s.close();
+      } catch (Exception e) {}
+    }
+  }
+
+
+
+  /**
+   * 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);
+
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "cn=Directory Manager",
+      "-w", "password",
+      "set-password-policy-prop",
+      "--policy-name", "Default Password Policy",
+      "--set", "force-change-on-add:true"
+    };
+    assertEquals(DSConfig.main(args, false, System.out, System.err), 0);
+
+    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());
+    ASN1Reader r = new ASN1Reader(s);
+    ASN1Writer w = new ASN1Writer(s);
+
+    try
+    {
+      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(), LDAPResultCode.SUCCESS);
+
+
+      SearchRequestProtocolOp searchRequest =
+           new SearchRequestProtocolOp(new ASN1OctetString("o=test"),
+                                       SearchScope.BASE_OBJECT,
+                                       DereferencePolicy.NEVER_DEREF_ALIASES, 0,
+                                       0, false,
+                                       LDAPFilter.decode("(objectClass=*)"),
+                                       new LinkedHashSet<String>());
+
+      ArrayList<LDAPControl> controls = new ArrayList<LDAPControl>();
+      controls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL, true));
+
+      message = new LDAPMessage(2, searchRequest, controls);
+      w.writeElement(message.encode());
+
+      message = LDAPMessage.decode(r.readElement().decodeAsSequence());
+      SearchResultDoneProtocolOp searchDone =
+           message.getSearchResultDoneProtocolOp();
+      assertFalse(searchDone.getResultCode() == LDAPResultCode.SUCCESS);
+
+      controls = message.getControls();
+      assertNotNull(controls);
+      assertFalse(controls.isEmpty());
+
+      boolean found = false;
+      for (LDAPControl c : controls)
+      {
+        if (c.getOID().equals(OID_PASSWORD_POLICY_CONTROL))
+        {
+          PasswordPolicyResponseControl pwpControl =
+               PasswordPolicyResponseControl.decodeControl(c.getControl());
+          assertEquals(pwpControl.getErrorType(),
+                       PasswordPolicyErrorType.CHANGE_AFTER_RESET);
+          found = true;
+        }
+      }
+      assertTrue(found);
+    }
+    finally
+    {
+      args = new String[]
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+        "-D", "cn=Directory Manager",
+        "-w", "password",
+        "set-password-policy-prop",
+        "--policy-name", "Default Password Policy",
+        "--set", "force-change-on-add:false"
+      };
+      assertEquals(DSConfig.main(args, false, System.out, System.err), 0);
+
+      try
+      {
+        s.close();
+      } catch (Exception e) {}
+    }
+  }
+}
+

--
Gitblit v1.10.0