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/src/server/org/opends/server/core/PasswordPolicyState.java |  621 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 621 insertions(+), 0 deletions(-)

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

--
Gitblit v1.10.0