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

neil_a_wilson
17.59.2007 2c7b8d6d8c0c177e8089272140dae66b87852ff7
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.

This commit also updates the manage-account tool so that it supports a public
option for displaying the password history state values, and a hidden option
for clearing a user's password history state.

OpenDS Issue Number: 339
15 files modified
1250 ■■■■■ changed files
opends/resource/config/config.ldif 4 ●●●● patch | view | raw | blame | history
opends/resource/schema/02-config.ldif 9 ●●●● patch | view | raw | blame | history
opends/src/admin/defn/org/opends/server/admin/std/PasswordPolicyConfiguration.xml 57 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/config/ConfigConstants.java 31 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/core/PasswordPolicy.java 47 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/core/PasswordPolicyState.java 621 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/extensions/PasswordModifyExtendedOperation.java 28 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/extensions/PasswordPolicyStateExtendedOperation.java 31 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/messages/CoreMessages.java 12 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/messages/ExtensionsMessages.java 13 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/messages/ToolMessages.java 34 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/tools/ManageAccount.java 37 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendWorkflowElement.java 27 ●●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/core/PasswordPolicyTestCase.java 289 ●●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/tools/ManageAccountTestCase.java 10 ●●●● patch | view | raw | blame | history
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
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 $
opends/src/admin/defn/org/opends/server/admin/std/PasswordPolicyConfiguration.xml
@@ -992,6 +992,63 @@
      <ldap:attribute>
        <ldap:oid>1.3.6.1.4.1.26027.1.1.443</ldap:oid>
        <ldap:name>ds-cfg-state-update-failure-policy</ldap:name>
      </ldap:attribute>
    </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>
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.
   */
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);
  }
}
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
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();
            }
          }
        }
      }
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);
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 " +
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");
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");
  }
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;
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();
            }
          }
        }
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()
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" }
    };
  }