From 2c7b8d6d8c0c177e8089272140dae66b87852ff7 Mon Sep 17 00:00:00 2001
From: neil_a_wilson <neil_a_wilson@localhost>
Date: Tue, 17 Jul 2007 21:59:32 +0000
Subject: [PATCH] Implement support for password history functionality.  The password history can be maintained either based on the number of previous passwords to remember (e.g., a user cannot re-use any of his/her last five passwords), or the length of time the previous passwords have been retained (e.g., a user cannot re-use any password he/she has had within the last 365 days), or both.

---
 opends/resource/schema/02-config.ldif                                                             |    9 
 opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendWorkflowElement.java |   27 +
 opends/src/server/org/opends/server/extensions/PasswordPolicyStateExtendedOperation.java          |   31 +
 opends/tests/unit-tests-testng/src/server/org/opends/server/core/PasswordPolicyTestCase.java      |  289 +++++++++++++
 opends/resource/config/config.ldif                                                                |    4 
 opends/src/server/org/opends/server/messages/CoreMessages.java                                    |   12 
 opends/src/server/org/opends/server/messages/ExtensionsMessages.java                              |   13 
 opends/src/server/org/opends/server/extensions/PasswordModifyExtendedOperation.java               |   28 +
 opends/src/server/org/opends/server/messages/ToolMessages.java                                    |   34 +
 opends/src/server/org/opends/server/tools/ManageAccount.java                                      |   37 +
 opends/src/server/org/opends/server/core/PasswordPolicyState.java                                 |  621 +++++++++++++++++++++++++++++
 opends/src/admin/defn/org/opends/server/admin/std/PasswordPolicyConfiguration.xml                 |   59 ++
 opends/src/server/org/opends/server/core/PasswordPolicy.java                                      |   47 ++
 opends/tests/unit-tests-testng/src/server/org/opends/server/tools/ManageAccountTestCase.java      |   10 
 opends/src/server/org/opends/server/config/ConfigConstants.java                                   |   31 +
 15 files changed, 1,247 insertions(+), 5 deletions(-)

diff --git a/opends/resource/config/config.ldif b/opends/resource/config/config.ldif
index 61302b1..003d635 100644
--- a/opends/resource/config/config.ldif
+++ b/opends/resource/config/config.ldif
@@ -1080,6 +1080,8 @@
 ds-cfg-require-secure-password-changes: false
 ds-cfg-skip-validation-for-administrators: false
 ds-cfg-state-update-failure-policy: reactive
+ds-cfg-password-history-count: 0
+ds-cfg-password-history-duration: 0 seconds
 
 dn: cn=Root Password Policy,cn=Password Policies,cn=config
 objectClass: top
@@ -1108,6 +1110,8 @@
 ds-cfg-require-secure-password-changes: false
 ds-cfg-skip-validation-for-administrators: false
 ds-cfg-state-update-failure-policy: ignore
+ds-cfg-password-history-count: 0
+ds-cfg-password-history-duration: 0 seconds
 
 dn: cn=Password Storage Schemes,cn=config
 objectClass: top
diff --git a/opends/resource/schema/02-config.ldif b/opends/resource/schema/02-config.ldif
index 00341bd..82bba2c 100644
--- a/opends/resource/schema/02-config.ldif
+++ b/opends/resource/schema/02-config.ldif
@@ -1478,6 +1478,12 @@
 attributeTypes: ( 1.3.6.1.4.1.26027.1.1.443
   NAME 'ds-cfg-state-update-failure-policy'
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'OpenDS Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.26027.1.1.444
+  NAME 'ds-cfg-password-history-count' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
+  X-ORIGIN 'OpenDS Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.26027.1.1.445
+  NAME 'ds-cfg-password-history-duration' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+  X-ORIGIN 'OpenDS Directory Server' )
 objectClasses: ( 1.3.6.1.4.1.26027.1.2.1
   NAME 'ds-cfg-access-control-handler' SUP top STRUCTURAL
   MUST ( cn $ ds-cfg-acl-handler-class $ ds-cfg-acl-handler-enabled )
@@ -1807,7 +1813,8 @@
   ds-cfg-require-change-by-time $ ds-cfg-require-secure-authentication $
   ds-cfg-require-secure-password-changes $
   ds-cfg-skip-validation-for-administrators $
-  ds-cfg-state-update-failure-policy ) X-ORIGIN 'OpenDS Directory Server' )
+  ds-cfg-state-update-failure-policy $ ds-cfg-password-history-count $
+  ds-cfg-password-history-duration ) X-ORIGIN 'OpenDS Directory Server' )
 objectClasses: ( 1.3.6.1.4.1.26027.1.2.63 NAME
   'ds-cfg-jmx-connection-handler' SUP ds-cfg-connection-handler
   STRUCTURAL MUST ( ds-cfg-listen-port $ ds-cfg-ssl-cert-nickname $
diff --git a/opends/src/admin/defn/org/opends/server/admin/std/PasswordPolicyConfiguration.xml b/opends/src/admin/defn/org/opends/server/admin/std/PasswordPolicyConfiguration.xml
index 933f525..447b814 100644
--- a/opends/src/admin/defn/org/opends/server/admin/std/PasswordPolicyConfiguration.xml
+++ b/opends/src/admin/defn/org/opends/server/admin/std/PasswordPolicyConfiguration.xml
@@ -946,7 +946,7 @@
   </adm:property>
 
   <adm:property name="state-update-failure-policy" mandatory="false"
-  multi-valued="false">
+    multi-valued="false">
     <adm:synopsis>
       Specifies how the server should deal with the inability to update password
       policy state information during an authentication attempt.  In particular,
@@ -996,5 +996,62 @@
     </adm:profile>
   </adm:property>
 
+  <adm:property name="password-history-count" mandatory="false"
+    multi-valued="false">
+    <adm:synopsis>
+      Specifies the maximum number of former passwords to maintain in the
+      password history.  When choosing a new password, the proposed password
+      will be checked to ensure that it does not match the current password, nor
+      any other password in the history list.  A value of zero indicates that
+      either no password history is to be maintained (if the password history
+      duration has a value of zero seconds), or that there is no maximum number
+      of passwords to maintain in the history (if the password history duration
+      has a value greater than zero seconds).
+    </adm:synopsis>
+    <adm:default-behavior>
+      <adm:defined>
+        <adm:value>0</adm:value>
+      </adm:defined>
+    </adm:default-behavior>
+    <adm:syntax>
+      <adm:integer lower-limit="0" upper-limit="2147483647" />
+    </adm:syntax>
+    <adm:profile name="ldap">
+      <ldap:attribute>
+        <ldap:oid>1.3.6.1.4.1.26027.1.1.444</ldap:oid>
+        <ldap:name>ds-cfg-password-history-count</ldap:name>
+      </ldap:attribute>
+    </adm:profile>
+  </adm:property>
+
+  <adm:property name="password-history-duration" mandatory="false"
+    multi-valued="false">
+    <adm:synopsis>
+      Specifies the maximum length of time that passwords should remain in the
+      password history.  When choosing a new password, the proposed password
+      will be checked to ensure that it does not match the current password, nor
+      any other password in the history list.  A value of zero seconds indicates
+      that either no password history is to be maintained (if the password
+      history count has a value of zero), or that there is no maximum duration
+      for passwords in the history (if the password history count has a value
+      greater than zero).
+    </adm:synopsis>
+    <adm:default-behavior>
+      <adm:defined>
+        <adm:value>0 seconds</adm:value>
+      </adm:defined>
+    </adm:default-behavior>
+    <adm:syntax>
+      <adm:duration base-unit="s" lower-limit="0" upper-limit="2147483647"
+        allow-unlimited="false" />
+    </adm:syntax>
+    <adm:profile name="ldap">
+      <ldap:attribute>
+        <ldap:oid>1.3.6.1.4.1.26027.1.1.445</ldap:oid>
+        <ldap:name>ds-cfg-password-history-duration</ldap:name>
+      </ldap:attribute>
+    </adm:profile>
+  </adm:property>
+
 </adm:managed-object>
 
diff --git a/opends/src/server/org/opends/server/config/ConfigConstants.java b/opends/src/server/org/opends/server/config/ConfigConstants.java
index ba2e5c2..818e728 100644
--- a/opends/src/server/org/opends/server/config/ConfigConstants.java
+++ b/opends/src/server/org/opends/server/config/ConfigConstants.java
@@ -1802,6 +1802,21 @@
 
 
   /**
+   * The default value for the password history count configuration attribute.
+   */
+  public static final int DEFAULT_PWPOLICY_HISTORY_COUNT = 0;
+
+
+
+  /**
+   * The default value for the password history duration configuration
+   * attribute, in seconds.
+   */
+  public static final int DEFAULT_PWPOLICY_HISTORY_DURATION = 0;
+
+
+
+  /**
    * The name of the configuration attribute that specifies the maximum length
    * of time an account may remain idle.
    */
@@ -3449,6 +3464,22 @@
 
 
   /**
+   * The name of the operational attribute that is used to maintain the password
+   * history for the user.
+   */
+  public static final String OP_ATTR_PWPOLICY_HISTORY = "pwdHistory";
+
+
+
+  /**
+   * The name of the operational attribute that is used to maintain the password
+   * history for the user, in all lowercase characters.
+   */
+  public static final String OP_ATTR_PWPOLICY_HISTORY_LC = "pwdhistory";
+
+
+
+  /**
    * The name of the operational attribute that specifies the time that the
    * account was locked due to too many failed attempts.
    */
diff --git a/opends/src/server/org/opends/server/core/PasswordPolicy.java b/opends/src/server/org/opends/server/core/PasswordPolicy.java
index 6e5eb90..37e40a4 100644
--- a/opends/src/server/org/opends/server/core/PasswordPolicy.java
+++ b/opends/src/server/org/opends/server/core/PasswordPolicy.java
@@ -175,6 +175,12 @@
   // The number of grace logins that a user may have.
   private int graceLoginCount = DEFAULT_PWPOLICY_GRACE_LOGIN_COUNT;
 
+  // The number of passwords to keep in the history.
+  private int historyCount = DEFAULT_PWPOLICY_HISTORY_COUNT;
+
+  // The maximum length of time in seconds to keep passwords in the history.
+  private int historyDuration = DEFAULT_PWPOLICY_HISTORY_DURATION;
+
   // The maximum length of time in seconds that an account may remain idle
   // before it is locked out.
   private int idleLockoutInterval = DEFAULT_PWPOLICY_IDLE_LOCKOUT_INTERVAL;
@@ -812,6 +818,11 @@
     this.stateUpdateFailurePolicy = configuration.getStateUpdateFailurePolicy();
 
 
+    // Get the password history count and duration.
+    this.historyCount    = configuration.getPasswordHistoryCount();
+    this.historyDuration = (int) configuration.getPasswordHistoryDuration();
+
+
     /*
      *  Holistic validation.
      */
@@ -1115,6 +1126,34 @@
 
 
   /**
+   * Retrieves the maximum number of previous passwords to maintain in the
+   * password history.
+   *
+   * @return  The maximum number of previous passwords to maintain in the
+   *          password history.
+   */
+  public int getPasswordHistoryCount()
+  {
+    return historyCount;
+  }
+
+
+
+  /**
+   * Retrieves the maximum length of time in seconds that previous passwords
+   * should remain in the password history.
+   *
+   * @return  The maximum length of time in seconds that previous passwords
+   *          should remain in the password history.
+   */
+  public int getPasswordHistoryDuration()
+  {
+    return historyDuration;
+  }
+
+
+
+  /**
    * Indicates whether users with this password policy will be required to
    * authenticate in a secure manner that does not expose their password.
    *
@@ -1739,6 +1778,14 @@
     buffer.append(idleLockoutInterval);
     buffer.append(" seconds");
     buffer.append(EOL);
+
+    buffer.append("History Count:                         ");
+    buffer.append(historyCount);
+    buffer.append(EOL);
+
+    buffer.append("Update Failure Policy:                 ");
+    buffer.append(stateUpdateFailurePolicy.toString());
+    buffer.append(EOL);
   }
 }
 
diff --git a/opends/src/server/org/opends/server/core/PasswordPolicyState.java b/opends/src/server/org/opends/server/core/PasswordPolicyState.java
index bd5be3f..a73bc74 100644
--- a/opends/src/server/org/opends/server/core/PasswordPolicyState.java
+++ b/opends/src/server/org/opends/server/core/PasswordPolicyState.java
@@ -38,6 +38,7 @@
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
+import java.util.TreeMap;
 
 import org.opends.server.admin.std.meta.PasswordPolicyCfgDefn;
 import org.opends.server.admin.std.server.PasswordValidatorCfg;
@@ -77,6 +78,7 @@
 import static org.opends.server.loggers.debug.DebugLogger.*;
 import static org.opends.server.messages.CoreMessages.*;
 import static org.opends.server.messages.MessageHandler.*;
+import static org.opends.server.schema.SchemaConstants.*;
 import static org.opends.server.util.StaticUtils.*;
 
 
@@ -3952,6 +3954,625 @@
 
 
   /**
+   * Indicates whether password history information should be matained for this
+   * user.
+   *
+   * @return  {@code true} if password history information should be maintained
+   *          for this user, or {@code false} if not.
+   */
+  public boolean maintainHistory()
+  {
+    return ((passwordPolicy.getPasswordHistoryCount() > 0) ||
+            (passwordPolicy.getPasswordHistoryDuration() > 0));
+  }
+
+
+
+  /**
+   * Indicates whether the provided password is equal to any of the current
+   * passwords, or any of the passwords in the history.
+   *
+   * @param  password  The password for which to make the determination.
+   *
+   * @return  {@code true} if the provided password is equal to any of the
+   *          current passwords or any of the passwords in the history, or
+   *          {@code false} if not.
+   */
+  public boolean isPasswordInHistory(ByteString password)
+  {
+    if (! maintainHistory())
+    {
+      if (debug)
+      {
+        if (debugEnabled())
+        {
+          TRACER.debugInfo("Returning false because password history " +
+                           "checking is disabled.");
+        }
+      }
+
+      // Password history checking is disabled, so we don't care if it is in the
+      // list or not.
+      return false;
+    }
+
+
+    // Check to see if the provided password is equal to any of the current
+    // passwords.  If so, then we'll consider it to be in the history.
+    if (passwordMatches(password))
+    {
+      if (debug)
+      {
+        if (debugEnabled())
+        {
+          TRACER.debugInfo("Returning true because the provided password " +
+                           "is currently in use.");
+        }
+      }
+
+      return true;
+    }
+
+
+    // Get the attribute containing the history and check to see if any of the
+    // values is equal to the provided password.  However, first prune the list
+    // by size and duration if necessary.
+    TreeMap<Long,AttributeValue> historyMap = getSortedHistoryValues(null);
+
+    int historyCount = passwordPolicy.getPasswordHistoryCount();
+    if ((historyCount > 0) && (historyMap.size() > historyCount))
+    {
+      int numToDelete = historyMap.size() - historyCount;
+      Iterator<Long> iterator = historyMap.keySet().iterator();
+      while ((iterator.hasNext()) && (numToDelete > 0))
+      {
+        iterator.next();
+        iterator.remove();
+        numToDelete--;
+      }
+    }
+
+    int historyDuration = passwordPolicy.getPasswordHistoryDuration();
+    if (historyDuration > 0)
+    {
+      long retainDate = currentTime - (1000 * historyDuration);
+      Iterator<Long> iterator = historyMap.keySet().iterator();
+      while (iterator.hasNext())
+      {
+        long historyDate = iterator.next();
+        if (historyDate < retainDate)
+        {
+          iterator.remove();
+        }
+        else
+        {
+          break;
+        }
+      }
+    }
+
+    for (AttributeValue v : historyMap.values())
+    {
+      if (historyValueMatches(password, v))
+      {
+        if (debug)
+        {
+          if (debugEnabled())
+          {
+            TRACER.debugInfo("Returning true because the password is in " +
+                             "the history.");
+          }
+        }
+
+        return true;
+      }
+    }
+
+
+    // If we've gotten here, then the password isn't in the history.
+    if (debug)
+    {
+      if (debugEnabled())
+      {
+        TRACER.debugInfo("Returning false because the password isn't in the " +
+                         "history.");
+      }
+    }
+
+    return false;
+  }
+
+
+
+  /**
+   * Gets a sorted list of the password history values contained in the user's
+   * entry.  The values will be sorted by timestamp.
+   *
+   * @param  removeAttrs  A list into which any values will be placed that could
+   *                      not be properly decoded.  It may be {@code null} if
+   *                      this is not needed.
+   */
+  private TreeMap<Long,AttributeValue> getSortedHistoryValues(List<Attribute>
+                                                                   removeAttrs)
+  {
+    TreeMap<Long,AttributeValue> historyMap =
+         new TreeMap<Long,AttributeValue>();
+    AttributeType historyType =
+         DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_HISTORY_LC, true);
+    List<Attribute> attrList = userEntry.getAttribute(historyType);
+    if (attrList != null)
+    {
+      for (Attribute a : attrList)
+      {
+        for (AttributeValue v : a.getValues())
+        {
+          String histStr = v.getStringValue();
+          int    hashPos = histStr.indexOf('#');
+          if (hashPos <= 0)
+          {
+            if (debug)
+            {
+              if (debugEnabled())
+              {
+                TRACER.debugInfo("Found value " + histStr + " in the " +
+                                 "history with no timestamp.  Marking it " +
+                                 "for removal.");
+              }
+            }
+
+            LinkedHashSet<AttributeValue> values =
+                 new LinkedHashSet<AttributeValue>(1);
+            values.add(v);
+            if (removeAttrs != null)
+            {
+              removeAttrs.add(new Attribute(a.getAttributeType(), a.getName(),
+                                            values));
+            }
+          }
+          else
+          {
+            try
+            {
+              long timestamp =
+                   GeneralizedTimeSyntax.decodeGeneralizedTimeValue(
+                        new ASN1OctetString(histStr.substring(0, hashPos)));
+              historyMap.put(timestamp, v);
+            }
+            catch (Exception e)
+            {
+              if (debugEnabled())
+              {
+                TRACER.debugCaught(DebugLogLevel.ERROR, e);
+
+                if (debug)
+                {
+                  TRACER.debugInfo("Could not decode the timestamp in " +
+                                   "history value " + histStr + " -- " + e +
+                                   ".  Marking it for removal.");
+                }
+              }
+
+              LinkedHashSet<AttributeValue> values =
+                   new LinkedHashSet<AttributeValue>(1);
+              values.add(v);
+              if (removeAttrs != null)
+              {
+                removeAttrs.add(new Attribute(a.getAttributeType(), a.getName(),
+                                              values));
+              }
+            }
+          }
+        }
+      }
+    }
+
+    return historyMap;
+  }
+
+
+
+  /**
+   * Indicates whether the provided password matches the given history value.
+   *
+   * @param  password      The clear-text password for which to make the
+   *                       determination.
+   * @param  historyValue  The encoded history value to compare against the
+   *                       clear-text password.
+   *
+   * @return  {@code true} if the provided password matches the history value,
+   *          or {@code false} if not.
+   */
+  private boolean historyValueMatches(ByteString password,
+                                      AttributeValue historyValue)
+  {
+    // According to draft-behera-ldap-password-policy, password history values
+    // should be in the format time#syntaxoid#encodedvalue.  In this method,
+    // we only care about the syntax OID and encoded password.
+    try
+    {
+      String histStr  = historyValue.getStringValue();
+      int    hashPos1 = histStr.indexOf('#');
+      if (hashPos1 <= 0)
+      {
+        if (debug)
+        {
+          if (debugEnabled())
+          {
+            TRACER.debugInfo("Returning false because the password history " +
+                             "value didn't include any hash characters.");
+          }
+        }
+
+        return false;
+      }
+
+      int hashPos2 = histStr.indexOf('#', hashPos1+1);
+      if (hashPos2 < 0)
+      {
+        if (debug)
+        {
+          if (debugEnabled())
+          {
+            TRACER.debugInfo("Returning false because the password history " +
+                             "value only had one hash character.");
+          }
+        }
+
+        return false;
+      }
+
+      String syntaxOID = toLowerCase(histStr.substring(hashPos1+1, hashPos2));
+      if (syntaxOID.equals(SYNTAX_AUTH_PASSWORD_OID))
+      {
+        StringBuilder[] authPWComponents =
+             AuthPasswordSyntax.decodeAuthPassword(
+                  histStr.substring(hashPos2+1));
+        PasswordStorageScheme scheme =
+             DirectoryServer.getAuthPasswordStorageScheme(
+                  authPWComponents[0].toString());
+        if (scheme.authPasswordMatches(password, authPWComponents[1].toString(),
+                                       authPWComponents[2].toString()))
+        {
+          if (debug)
+          {
+            if (debugEnabled())
+            {
+              TRACER.debugInfo("Returning true because the auth password " +
+                               "history value matched.");
+            }
+          }
+
+          return true;
+        }
+        else
+        {
+          if (debug)
+          {
+            if (debugEnabled())
+            {
+              TRACER.debugInfo("Returning false because the auth password " +
+                               "history value did not match.");
+            }
+          }
+
+          return false;
+        }
+      }
+      else if (syntaxOID.equals(SYNTAX_USER_PASSWORD_OID))
+      {
+        String[] userPWComponents =
+             UserPasswordSyntax.decodeUserPassword(
+                  histStr.substring(hashPos2+1));
+        PasswordStorageScheme scheme =
+             DirectoryServer.getPasswordStorageScheme(userPWComponents[0]);
+        if (scheme.passwordMatches(password,
+                                   new ASN1OctetString(userPWComponents[1])))
+        {
+          if (debug)
+          {
+            if (debugEnabled())
+            {
+              TRACER.debugInfo("Returning true because the user password " +
+                               "history value matched.");
+            }
+          }
+
+          return true;
+        }
+        else
+        {
+          if (debug)
+          {
+            if (debugEnabled())
+            {
+              TRACER.debugInfo("Returning false because the user password " +
+                               "history value did not match.");
+            }
+          }
+
+          return false;
+        }
+      }
+      else
+      {
+        if (debug)
+        {
+          if (debugEnabled())
+          {
+            TRACER.debugInfo("Returning false because the syntax OID " +
+                             syntaxOID + " didn't match for either the auth " +
+                             "or user password syntax.");
+          }
+        }
+
+        return false;
+      }
+    }
+    catch (Exception e)
+    {
+      if (debugEnabled())
+      {
+        TRACER.debugCaught(DebugLogLevel.ERROR, e);
+
+        if (debug)
+        {
+          TRACER.debugInfo("Returning false because of an exception:  " +
+                           stackTraceToSingleLineString(e));
+        }
+      }
+
+      return false;
+    }
+  }
+
+
+
+  /**
+   * Updates the password history information for this user by adding all
+   * current passwords to it.
+   */
+  public void updatePasswordHistory()
+  {
+    List<Attribute> attrList =
+         userEntry.getAttribute(passwordPolicy.getPasswordAttribute());
+    if (attrList != null)
+    {
+      for (Attribute a : attrList)
+      {
+        for (AttributeValue v : a.getValues())
+        {
+          addPasswordToHistory(v.getStringValue());
+        }
+      }
+    }
+  }
+
+
+
+  /**
+   * Adds the provided password to the password history.  If appropriate, one or
+   * more old passwords may be evicted from the list if the total size would
+   * exceed the configured count, or if passwords are older than the configured
+   * duration.
+   *
+   * @param  encodedPassword  The encoded password (in either user password or
+   *                          auth password format) to be added to the history.
+   */
+  private void addPasswordToHistory(String encodedPassword)
+  {
+    if (! maintainHistory())
+    {
+      if (debug)
+      {
+        if (debugEnabled())
+        {
+          TRACER.debugInfo("Not doing anything because password history " +
+                           "maintenance is disabled.");
+        }
+      }
+
+      return;
+    }
+
+
+    // Get a sorted list of the existing values to see if there are any that
+    // should be removed.
+    LinkedList<Attribute> removeAttrs = new LinkedList<Attribute>();
+    TreeMap<Long,AttributeValue> historyMap =
+         getSortedHistoryValues(removeAttrs);
+
+
+    // If there is a maximum number of values to retain and we would be over the
+    // limit with the new value, then get rid of enough values (oldest first)
+    // to satisfy the count.
+    AttributeType historyType =
+         DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_HISTORY_LC, true);
+    int historyCount = passwordPolicy.getPasswordHistoryCount();
+    if  ((historyCount > 0) && (historyMap.size() >= historyCount))
+    {
+      int numToDelete = (historyMap.size() - historyCount) + 1;
+      LinkedHashSet<AttributeValue> removeValues =
+           new LinkedHashSet<AttributeValue>(numToDelete);
+      Iterator<AttributeValue> iterator = historyMap.values().iterator();
+      while (iterator.hasNext() && (numToDelete > 0))
+      {
+        AttributeValue v = iterator.next();
+        removeValues.add(v);
+        iterator.remove();
+        numToDelete--;
+
+        if (debug)
+        {
+          if (debugEnabled())
+          {
+            TRACER.debugInfo("Removing history value " + v.getStringValue() +
+                             " to preserve the history count.");
+          }
+        }
+      }
+
+      if (! removeValues.isEmpty())
+      {
+        removeAttrs.add(new Attribute(historyType, historyType.getPrimaryName(),
+                                      removeValues));
+      }
+    }
+
+
+    // If there is a maximum duration, then get rid of any values that would be
+    // over the duration.
+    int historyDuration = passwordPolicy.getPasswordHistoryDuration();
+    if (historyDuration > 0)
+    {
+      long minAgeToKeep = currentTime - (1000L * historyDuration);
+      Iterator<Long> iterator = historyMap.keySet().iterator();
+      LinkedHashSet<AttributeValue> removeValues =
+           new LinkedHashSet<AttributeValue>();
+      while (iterator.hasNext())
+      {
+        long timestamp = iterator.next();
+        if (timestamp < minAgeToKeep)
+        {
+          AttributeValue v = historyMap.get(timestamp);
+          removeValues.add(v);
+          iterator.remove();
+
+          if (debug)
+          {
+            if (debugEnabled())
+            {
+              TRACER.debugInfo("Removing history value " + v.getStringValue() +
+                               " to preserve the history duration.");
+            }
+          }
+        }
+        else
+        {
+          break;
+        }
+      }
+
+      if (! removeValues.isEmpty())
+      {
+        removeAttrs.add(new Attribute(historyType, historyType.getPrimaryName(),
+                                      removeValues));
+      }
+    }
+
+
+    // At this point, we can add the new value.  However, we want to make sure
+    // that its timestamp (which is the current time) doesn't conflict with any
+    // value already in the list.  If there is a conflict, then simply add one
+    // to it until we don't have any more conflicts.
+    long newTimestamp = currentTime;
+    while (historyMap.containsKey(newTimestamp))
+    {
+      newTimestamp++;
+    }
+    String newHistStr = GeneralizedTimeSyntax.format(newTimestamp) + "#" +
+                        passwordPolicy.getPasswordAttribute().getSyntaxOID() +
+                        "#" + encodedPassword;
+    LinkedHashSet<AttributeValue> newHistValues =
+         new LinkedHashSet<AttributeValue>(1);
+    newHistValues.add(new AttributeValue(historyType, newHistStr));
+    Attribute newHistAttr =
+         new Attribute(historyType, historyType.getPrimaryName(),
+                       newHistValues);
+
+    if (debug)
+    {
+      if (debugEnabled())
+      {
+        TRACER.debugInfo("Going to add history value " + newHistStr);
+      }
+    }
+
+
+    // Apply the changes, either by adding modifications or by directly updating
+    // the entry.
+    if (updateEntry)
+    {
+      LinkedList<AttributeValue> valueList = new LinkedList<AttributeValue>();
+      for (Attribute a : removeAttrs)
+      {
+        userEntry.removeAttribute(a, valueList);
+      }
+
+      userEntry.addAttribute(newHistAttr, valueList);
+    }
+    else
+    {
+      for (Attribute a : removeAttrs)
+      {
+        modifications.add(new Modification(ModificationType.DELETE, a, true));
+      }
+
+      modifications.add(new Modification(ModificationType.ADD, newHistAttr,
+                                         true));
+    }
+  }
+
+
+
+  /**
+   * Retrieves the password history state values for the user.  This is only
+   * intended for testing purposes.
+   *
+   * @return  The password history state values for the user.
+   */
+  public String[] getPasswordHistoryValues()
+  {
+    ArrayList<String> historyValues = new ArrayList<String>();
+    AttributeType historyType =
+         DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_HISTORY_LC, true);
+    List<Attribute> attrList = userEntry.getAttribute(historyType);
+    if (attrList != null)
+    {
+      for (Attribute a : attrList)
+      {
+        for (AttributeValue v : a.getValues())
+        {
+          historyValues.add(v.getStringValue());
+        }
+      }
+    }
+
+    String[] historyArray = new String[historyValues.size()];
+    return historyValues.toArray(historyArray);
+  }
+
+
+
+  /**
+   * Clears the password history state information for the user.  This is only
+   * intended for testing purposes.
+   */
+  public void clearPasswordHistory()
+  {
+    if (debug)
+    {
+      if (debugEnabled())
+      {
+        TRACER.debugInfo("Clearing password history for user %s", userDNString);
+      }
+    }
+
+    AttributeType type = DirectoryServer.getAttributeType(
+                             OP_ATTR_PWPOLICY_HISTORY_LC, true);
+    if (updateEntry)
+    {
+      userEntry.removeAttribute(type);
+    }
+    else
+    {
+      modifications.add(new Modification(ModificationType.REPLACE,
+                                         new Attribute(type), true));
+    }
+  }
+
+
+
+  /**
    * Generates a new password for the user.
    *
    * @return  The new password that has been generated, or <CODE>null</CODE> if
diff --git a/opends/src/server/org/opends/server/extensions/PasswordModifyExtendedOperation.java b/opends/src/server/org/opends/server/extensions/PasswordModifyExtendedOperation.java
index 47bfac2..2676af9 100644
--- a/opends/src/server/org/opends/server/extensions/PasswordModifyExtendedOperation.java
+++ b/opends/src/server/org/opends/server/extensions/PasswordModifyExtendedOperation.java
@@ -861,6 +861,7 @@
         }
         else
         {
+          // Run the new password through the set of password validators.
           if (selfChange ||
                (! pwPolicyState.getPolicy().skipValidationForAdministrators()))
           {
@@ -919,6 +920,33 @@
               return;
             }
           }
+
+
+          // Prepare to update the password history, if necessary.
+          if (pwPolicyState.maintainHistory())
+          {
+            if (pwPolicyState.isPasswordInHistory(newPassword))
+            {
+              if (oldPassword == null)
+              {
+                operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+
+                int msgID = MSGID_EXTOP_PASSMOD_PW_IN_HISTORY;
+                operation.appendErrorMessage(getMessage(msgID));
+              }
+              else
+              {
+                operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
+
+                int msgID = MSGID_EXTOP_PASSMOD_PW_IN_HISTORY;
+                operation.appendAdditionalLogMessage(getMessage(msgID));
+              }
+            }
+            else
+            {
+              pwPolicyState.updatePasswordHistory();
+            }
+          }
         }
       }
 
diff --git a/opends/src/server/org/opends/server/extensions/PasswordPolicyStateExtendedOperation.java b/opends/src/server/org/opends/server/extensions/PasswordPolicyStateExtendedOperation.java
index a8c6deb..297b64a 100644
--- a/opends/src/server/org/opends/server/extensions/PasswordPolicyStateExtendedOperation.java
+++ b/opends/src/server/org/opends/server/extensions/PasswordPolicyStateExtendedOperation.java
@@ -121,6 +121,8 @@
  *           setPasswordChangedByRequiredTime             (36),
  *           clearPasswordChangedByRequiredTime           (37),
  *           getSecondsUntilRequiredChangeTime            (38),
+ *           getPasswordHistory                           (39),
+ *           clearPasswordHistory                         (40),
  *           ... },
  *      opValues     SEQUENCE OF OCTET STRING OPTIONAL }
  * </PRE>
@@ -420,6 +422,20 @@
 
 
 
+  /**
+   * The enumerated value for the getPasswordHistory operation.
+   */
+  public static final int OP_GET_PASSWORD_HISTORY = 39;
+
+
+
+  /**
+   * The enumerated value for the clearPasswordHistory operation.
+   */
+  public static final int OP_CLEAR_PASSWORD_HISTORY = 40;
+
+
+
   // The set of attributes to request when retrieving a user's entry.
   private LinkedHashSet<String> requestAttributes;
 
@@ -1225,6 +1241,15 @@
             returnTypes.add(OP_GET_SECONDS_UNTIL_REQUIRED_CHANGE_TIME);
             break;
 
+          case OP_GET_PASSWORD_HISTORY:
+            returnTypes.add(OP_GET_PASSWORD_HISTORY);
+            break;
+
+          case OP_CLEAR_PASSWORD_HISTORY:
+            pwpState.clearPasswordHistory();
+            returnTypes.add(OP_GET_PASSWORD_HISTORY);
+            break;
+
           default:
             int msgID = MSGID_PWPSTATE_EXTOP_UNKNOWN_OP_TYPE;
             operation.appendErrorMessage(getMessage(msgID, opType));
@@ -1604,6 +1629,12 @@
                             secondsStr));
     }
 
+    if (returnAll || returnTypes.contains(OP_GET_PASSWORD_HISTORY))
+    {
+      opElements.add(encode(OP_GET_PASSWORD_HISTORY,
+                            pwpState.getPasswordHistoryValues()));
+    }
+
     ArrayList<ASN1Element> responseValueElements =
          new ArrayList<ASN1Element>(2);
     responseValueElements.add(dnString);
diff --git a/opends/src/server/org/opends/server/messages/CoreMessages.java b/opends/src/server/org/opends/server/messages/CoreMessages.java
index 470fb15..1c0331c 100644
--- a/opends/src/server/org/opends/server/messages/CoreMessages.java
+++ b/opends/src/server/org/opends/server/messages/CoreMessages.java
@@ -6269,6 +6269,15 @@
 
 
   /**
+   * The message ID for the message that will be used if a new password is found
+   * in the password history.  This does not take any arguments.
+   */
+  public static final int MSGID_MODIFY_PW_IN_HISTORY =
+       CATEGORY_MASK_CORE | SEVERITY_MASK_MILD_ERROR | 629;
+
+
+
+  /**
    * Associates a set of generic messages with the message IDs defined
    * in this class.
    */
@@ -7504,6 +7513,9 @@
     registerMessage(MSGID_MODIFY_PW_VALIDATION_FAILED,
                     "The provided password value was rejected by a password " +
                     "validator:  %s");
+    registerMessage(MSGID_MODIFY_PW_IN_HISTORY,
+                    "The provided new password was found in the password " +
+                    "history for the user");
     registerMessage(MSGID_MODIFY_INCREMENT_REQUIRES_INTEGER_VALUE,
                     "Entry %s cannot be modified because an attempt was " +
                     "made to increment the value of attribute %s but the " +
diff --git a/opends/src/server/org/opends/server/messages/ExtensionsMessages.java b/opends/src/server/org/opends/server/messages/ExtensionsMessages.java
index 36d4a23..e4006f6 100644
--- a/opends/src/server/org/opends/server/messages/ExtensionsMessages.java
+++ b/opends/src/server/org/opends/server/messages/ExtensionsMessages.java
@@ -5502,6 +5502,16 @@
 
 
   /**
+   * The message ID for the message that will be used if a password change is
+   * rejected because the new password was found in the password history.  This
+   * does not take any arguments.
+   */
+  public static final int MSGID_EXTOP_PASSMOD_PW_IN_HISTORY =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 530;
+
+
+
+  /**
    * Associates a set of generic messages with the message IDs defined in this
    * class.
    */
@@ -5786,6 +5796,9 @@
     registerMessage(MSGID_EXTOP_PASSMOD_UNACCEPTABLE_PW,
                     "The provided new password failed the validation checks " +
                     "defined in the server:  %s");
+    registerMessage(MSGID_EXTOP_PASSMOD_PW_IN_HISTORY,
+                    "The provided new password was already contained in the " +
+                    "password history");
     registerMessage(MSGID_EXTOP_PASSMOD_CANNOT_ENCODE_PASSWORD,
                     "Unable to encode the provided password using the " +
                     "default scheme(s):  %s");
diff --git a/opends/src/server/org/opends/server/messages/ToolMessages.java b/opends/src/server/org/opends/server/messages/ToolMessages.java
index 40cd98d..5fcaf8b 100644
--- a/opends/src/server/org/opends/server/messages/ToolMessages.java
+++ b/opends/src/server/org/opends/server/messages/ToolMessages.java
@@ -9036,6 +9036,33 @@
 
 
   /**
+   * The message ID for the message that will be used as the label when
+   * displaying the password history state values.
+   */
+  public static final int MSGID_PWPSTATE_LABEL_PASSWORD_HISTORY =
+       CATEGORY_MASK_TOOLS | SEVERITY_MASK_INFORMATIONAL | 1200;
+
+
+
+  /**
+   * The message ID for the message that will be used as the description for the
+   * get-password-history subcommand.
+   */
+  public static final int MSGID_DESCRIPTION_PWPSTATE_GET_PASSWORD_HISTORY =
+       CATEGORY_MASK_TOOLS | SEVERITY_MASK_INFORMATIONAL | 1201;
+
+
+
+  /**
+   * The message ID for the message that will be used as the description for the
+   * clear-password-history subcommand.
+   */
+  public static final int MSGID_DESCRIPTION_PWPSTATE_CLEAR_PASSWORD_HISTORY =
+       CATEGORY_MASK_TOOLS | SEVERITY_MASK_INFORMATIONAL | 1202;
+
+
+
+  /**
    * Associates a set of generic messages with the message IDs defined in this
    * class.
    */
@@ -11867,6 +11894,11 @@
          "Display the length of time in seconds that the user has remaining " +
          "to change his or her password before the account becomes locked " +
          "due to the required change time");
+    registerMessage(MSGID_DESCRIPTION_PWPSTATE_GET_PASSWORD_HISTORY,
+                    "Display password history state values for the user");
+    registerMessage(MSGID_DESCRIPTION_PWPSTATE_CLEAR_PASSWORD_HISTORY,
+                    "Clear password history state values for the user.  This " +
+                    "should be used only for testing purposes");
     registerMessage(MSGID_PWPSTATE_MUTUALLY_EXCLUSIVE_ARGUMENTS,
                     "ERROR:  You may not provide both the %s and the %s " +
                     "arguments");
@@ -11951,6 +11983,8 @@
                     "Password Changed by Required Time");
     registerMessage(MSGID_PWPSTATE_LABEL_SECONDS_UNTIL_REQUIRED_CHANGE_TIME,
                     "Seconds Until Required Change Time");
+    registerMessage(MSGID_PWPSTATE_LABEL_PASSWORD_HISTORY,
+                    "Password History");
     registerMessage(MSGID_PWPSTATE_INVALID_RESPONSE_OP_TYPE,
                     "Unrecognized or invalid operation type:  %s");
   }
diff --git a/opends/src/server/org/opends/server/tools/ManageAccount.java b/opends/src/server/org/opends/server/tools/ManageAccount.java
index 7c9b52d..9ab38c7 100644
--- a/opends/src/server/org/opends/server/tools/ManageAccount.java
+++ b/opends/src/server/org/opends/server/tools/ManageAccount.java
@@ -441,6 +441,23 @@
 
 
   /**
+   * The name of the subcommand that will be used to get the password history
+   * state values.
+   */
+  private static final String SC_GET_PASSWORD_HISTORY = "get-password-history";
+
+
+
+  /**
+   * The name of the subcommand that will be used to clear the password history
+   * state values.
+   */
+  private static final String SC_CLEAR_PASSWORD_HISTORY =
+       "clear-password-history";
+
+
+
+  /**
    * The name of the argument that will be used for holding the value(s) to use
    * for the target operation.
    */
@@ -760,6 +777,11 @@
             printLabelAndValues(msgID, opValues);
             break;
 
+          case OP_GET_PASSWORD_HISTORY:
+            msgID = MSGID_PWPSTATE_LABEL_PASSWORD_HISTORY;
+            printLabelAndValues(msgID, opValues);
+            break;
+
           default:
             msgID = MSGID_PWPSTATE_INVALID_RESPONSE_OP_TYPE;
             String message = getMessage(msgID, opType);
@@ -1130,6 +1152,13 @@
       msgID = MSGID_DESCRIPTION_PWPSTATE_GET_SECONDS_UNTIL_REQUIRED_CHANGE_TIME;
       new SubCommand(argParser, SC_GET_SECONDS_UNTIL_REQUIRED_CHANGE_TIME,
                      msgID);
+
+      msgID = MSGID_DESCRIPTION_PWPSTATE_GET_PASSWORD_HISTORY;
+      new SubCommand(argParser, SC_GET_PASSWORD_HISTORY, msgID);
+
+      msgID = MSGID_DESCRIPTION_PWPSTATE_CLEAR_PASSWORD_HISTORY;
+      sc = new SubCommand(argParser, SC_CLEAR_PASSWORD_HISTORY, msgID);
+      sc.setHidden(true);
     }
     catch (ArgumentException ae)
     {
@@ -1645,6 +1674,14 @@
       opElements.add(encode(OP_GET_SECONDS_UNTIL_REQUIRED_CHANGE_TIME,
                             NO_VALUE));
     }
+    else if (subCommandName.equals(SC_GET_PASSWORD_HISTORY))
+    {
+      opElements.add(encode(OP_GET_PASSWORD_HISTORY, NO_VALUE));
+    }
+    else if (subCommandName.equals(SC_CLEAR_PASSWORD_HISTORY))
+    {
+      opElements.add(encode(OP_CLEAR_PASSWORD_HISTORY, NO_VALUE));
+    }
     else
     {
       msgID = MSGID_PWPSTATE_INVALID_SUBCOMMAND;
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 2012bee..f440150 100644
--- a/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendWorkflowElement.java
+++ b/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendWorkflowElement.java
@@ -1869,6 +1869,33 @@
               }
             }
           }
+
+
+          // If we should check the password history, then do so now.
+          if (pwPolicyState.maintainHistory())
+          {
+            List<AttributeValue> newPasswords = localOp.getNewPasswords();
+            if (newPasswords != null)
+            {
+              for (AttributeValue v : newPasswords)
+              {
+                if (pwPolicyState.isPasswordInHistory(v.getValue()))
+                {
+                  if (selfChange || (! pwPolicyState.getPolicy().
+                                            skipValidationForAdministrators()))
+                  {
+                    localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+
+                    int msgID = MSGID_MODIFY_PW_IN_HISTORY;
+                    localOp.appendErrorMessage(getMessage(msgID));
+                    break modifyProcessing;
+                  }
+                }
+              }
+
+              pwPolicyState.updatePasswordHistory();
+            }
+          }
         }
 
 
diff --git a/opends/tests/unit-tests-testng/src/server/org/opends/server/core/PasswordPolicyTestCase.java b/opends/tests/unit-tests-testng/src/server/org/opends/server/core/PasswordPolicyTestCase.java
index 6384d01..85d6569 100644
--- a/opends/tests/unit-tests-testng/src/server/org/opends/server/core/PasswordPolicyTestCase.java
+++ b/opends/tests/unit-tests-testng/src/server/org/opends/server/core/PasswordPolicyTestCase.java
@@ -4822,6 +4822,295 @@
 
 
   /**
+   * Tests the Directory Server's password history maintenance capabilities
+   * using only the password history count configuration option.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testPasswordHistoryUsingCount()
+         throws Exception
+  {
+    TestCaseUtils.initializeTestBackend(true);
+    TestCaseUtils.addEntry(
+      "dn: uid=test.user,o=test",
+      "objectClass: top",
+      "objectClass: person",
+      "objectClass: organizationalPerson",
+      "objectClass: inetOrgPerson",
+      "uid: test.user",
+      "givenName: Test",
+      "sn: User",
+      "cn: Test User",
+      "userPassword: originalPassword",
+      "ds-privilege-name: bypass-acl");
+
+    // Make sure that before we enable history features we can re-use the
+    // current password.
+    String origPWPath = TestCaseUtils.createTempFile(
+      "dn: uid=test.user,o=test",
+      "changetype: modify",
+      "replace: userPassword",
+      "userPassword: originalPassword");
+
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "uid=test.user,o=test",
+      "-w", "originalPassword",
+      "-f", origPWPath
+    };
+
+    assertEquals(LDAPModify.mainModify(args, false, System.out, System.err), 0);
+
+    TestCaseUtils.applyModifications(
+      "dn: cn=Default Password Policy,cn=Password Policies,cn=config",
+      "changetype: modify",
+      "replace: ds-cfg-password-history-count",
+      "ds-cfg-password-history-count: 3");
+
+    try
+    {
+      // Make sure that we cannot re-use the original password as a new
+      // password.
+      assertFalse(LDAPModify.mainModify(args, false, System.out, System.err) ==
+                  0);
+
+
+      // Change the password three times.
+      String newPWsPath = TestCaseUtils.createTempFile(
+        "dn: uid=test.user,o=test",
+        "changetype: modify",
+        "replace: userPassword",
+        "userPassword: newPassword1",
+        "",
+        "dn: uid=test.user,o=test",
+        "changetype: modify",
+        "replace: userPassword",
+        "userPassword: newPassword2",
+        "",
+        "dn: uid=test.user,o=test",
+        "changetype: modify",
+        "replace: userPassword",
+        "userPassword: newPassword3");
+
+      args = new String[]
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+        "-D", "uid=test.user,o=test",
+        "-w", "originalPassword",
+        "-f", newPWsPath
+      };
+
+      assertEquals(LDAPModify.mainModify(args, false, System.out, System.err),
+                   0);
+
+
+      // Make sure that we still can't use the original password.
+      args = new String[]
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+        "-D", "uid=test.user,o=test",
+        "-w", "newPassword3",
+        "-f", origPWPath
+      };
+
+      assertFalse(LDAPModify.mainModify(args, false, System.out, System.err) ==
+                  0);
+
+
+      // Change the password one more time and then verify that we can use the
+      // original password again.
+      String newPWsPath2 = TestCaseUtils.createTempFile(
+        "dn: uid=test.user,o=test",
+        "changetype: modify",
+        "replace: userPassword",
+        "userPassword: newPassword4",
+        "",
+        "dn: uid=test.user,o=test",
+        "changetype: modify",
+        "replace: userPassword",
+        "userPassword: originalPassword");
+
+      args = new String[]
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+        "-D", "uid=test.user,o=test",
+        "-w", "newPassword3",
+        "-f", newPWsPath2
+      };
+
+      assertEquals(LDAPModify.mainModify(args, false, System.out, System.err),
+                   0);
+
+
+      // Make sure that we can't use the first new password.
+      String firstPWPath = TestCaseUtils.createTempFile(
+        "dn: uid=test.user,o=test",
+        "changetype: modify",
+        "replace: userPassword",
+        "userPassword: newPassword1");
+
+      args = new String[]
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+        "-D", "uid=test.user,o=test",
+        "-w", "originalPassword",
+        "-f", firstPWPath
+      };
+
+      assertFalse(LDAPModify.mainModify(args, false, System.out, System.err) ==
+                  0);
+
+
+      // Reduce the password history count from 3 to 2 and verify that we can
+      // now use the first new password.
+      TestCaseUtils.applyModifications(
+        "dn: cn=Default Password Policy,cn=Password Policies,cn=config",
+        "changetype: modify",
+        "replace: ds-cfg-password-history-count",
+        "ds-cfg-password-history-count: 0");
+
+      assertEquals(LDAPModify.mainModify(args, false, System.out, System.err),
+                   0);
+    }
+    finally
+    {
+      TestCaseUtils.applyModifications(
+        "dn: cn=Default Password Policy,cn=Password Policies,cn=config",
+        "changetype: modify",
+        "replace: ds-cfg-password-history-count",
+        "ds-cfg-password-history-count: 0");
+    }
+  }
+
+
+
+  /**
+   * Tests the Directory Server's password history maintenance capabilities
+   * using only the password history duration configuration option.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test(groups = "slow")
+  public void testPasswordHistoryUsingDuration()
+         throws Exception
+  {
+    TestCaseUtils.initializeTestBackend(true);
+    TestCaseUtils.addEntry(
+      "dn: uid=test.user,o=test",
+      "objectClass: top",
+      "objectClass: person",
+      "objectClass: organizationalPerson",
+      "objectClass: inetOrgPerson",
+      "uid: test.user",
+      "givenName: Test",
+      "sn: User",
+      "cn: Test User",
+      "userPassword: originalPassword",
+      "ds-privilege-name: bypass-acl");
+
+    // Make sure that before we enable history features we can re-use the
+    // current password.
+    String origPWPath = TestCaseUtils.createTempFile(
+      "dn: uid=test.user,o=test",
+      "changetype: modify",
+      "replace: userPassword",
+      "userPassword: originalPassword");
+
+    String[] args =
+    {
+      "-h", "127.0.0.1",
+      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+      "-D", "uid=test.user,o=test",
+      "-w", "originalPassword",
+      "-f", origPWPath
+    };
+
+    assertEquals(LDAPModify.mainModify(args, false, System.out, System.err), 0);
+
+    TestCaseUtils.applyModifications(
+      "dn: cn=Default Password Policy,cn=Password Policies,cn=config",
+      "changetype: modify",
+      "replace: ds-cfg-password-history-duration",
+      "ds-cfg-password-history-duration: 5 seconds");
+
+    try
+    {
+      // Make sure that we can no longer re-use the original password as a new
+      // password.
+      assertFalse(LDAPModify.mainModify(args, false, System.out, System.err) ==
+                  0);
+
+
+      // Change the password three times.
+      String newPWsPath = TestCaseUtils.createTempFile(
+        "dn: uid=test.user,o=test",
+        "changetype: modify",
+        "replace: userPassword",
+        "userPassword: newPassword1",
+        "",
+        "dn: uid=test.user,o=test",
+        "changetype: modify",
+        "replace: userPassword",
+        "userPassword: newPassword2",
+        "",
+        "dn: uid=test.user,o=test",
+        "changetype: modify",
+        "replace: userPassword",
+        "userPassword: newPassword3");
+
+      args = new String[]
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+        "-D", "uid=test.user,o=test",
+        "-w", "originalPassword",
+        "-f", newPWsPath
+      };
+
+      assertEquals(LDAPModify.mainModify(args, false, System.out, System.err),
+                   0);
+
+
+      // Make sure that we still can't use the original password.
+      args = new String[]
+      {
+        "-h", "127.0.0.1",
+        "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
+        "-D", "uid=test.user,o=test",
+        "-w", "newPassword3",
+        "-f", origPWPath
+      };
+
+      assertFalse(LDAPModify.mainModify(args, false, System.out, System.err) ==
+                  0);
+
+
+      // Sleep for six seconds and then verify that we can use the original
+      // password again.
+      Thread.sleep(6000);
+      assertEquals(LDAPModify.mainModify(args, false, System.out, System.err),
+                  0);
+    }
+    finally
+    {
+      TestCaseUtils.applyModifications(
+        "dn: cn=Default Password Policy,cn=Password Policies,cn=config",
+        "changetype: modify",
+        "replace: ds-cfg-password-history-duration",
+        "ds-cfg-password-history-duration: 0 seconds");
+    }
+  }
+
+
+
+  /**
    * Tests the <CODE>toString</CODE> methods with the default password policy.
    */
   @Test()
diff --git a/opends/tests/unit-tests-testng/src/server/org/opends/server/tools/ManageAccountTestCase.java b/opends/tests/unit-tests-testng/src/server/org/opends/server/tools/ManageAccountTestCase.java
index 0ca38f2..8635359 100644
--- a/opends/tests/unit-tests-testng/src/server/org/opends/server/tools/ManageAccountTestCase.java
+++ b/opends/tests/unit-tests-testng/src/server/org/opends/server/tools/ManageAccountTestCase.java
@@ -110,7 +110,9 @@
       new Object[] { "get-password-changed-by-required-time" },
       new Object[] { "set-password-changed-by-required-time" },
       new Object[] { "clear-password-changed-by-required-time" },
-      new Object[] { "get-seconds-until-required-change-time" }
+      new Object[] { "get-seconds-until-required-change-time" },
+      new Object[] { "get-password-history" },
+      new Object[] { "clear-password-history" }
     };
   }
 
@@ -147,7 +149,8 @@
       new Object[] { "get-grace-login-use-times" },
       new Object[] { "get-remaining-grace-login-count" },
       new Object[] { "get-password-changed-by-required-time" },
-      new Object[] { "get-seconds-until-required-change-time" }
+      new Object[] { "get-seconds-until-required-change-time" },
+      new Object[] { "get-password-history" }
     };
   }
 
@@ -219,7 +222,8 @@
       new Object[] { "clear-last-login-time" },
       new Object[] { "clear-password-is-reset" },
       new Object[] { "clear-grace-login-use-times" },
-      new Object[] { "clear-password-changed-by-required-time" }
+      new Object[] { "clear-password-changed-by-required-time" },
+      new Object[] { "clear-password-history" }
     };
   }
 

--
Gitblit v1.10.0