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

Chris Ridd
24.45.2014 4a65d9e05332aa0634fde163a57b2953bbd4d60d
Forward port fix for OPENDJ-1497: pwdHistory stops updating when multiple storage schemes are used
2 files modified
275 ■■■■ changed files
opendj3-server-dev/src/server/org/opends/server/core/PasswordPolicyState.java 73 ●●●●● patch | view | raw | blame | history
opendj3-server-dev/tests/unit-tests-testng/src/server/org/opends/server/core/PasswordPolicyTestCase.java 202 ●●●● patch | view | raw | blame | history
opendj3-server-dev/src/server/org/opends/server/core/PasswordPolicyState.java
@@ -255,6 +255,30 @@
  }
  /**
   * Get the password storage scheme used by a given password value.
   *
   * @param  v  The encoded password value to check.
   *
   * @return  The scheme used by the password.
   *
   * @throws  DirectoryException  If the password could not be decoded.
   */
  private PasswordStorageScheme<?> getPasswordStorageScheme(ByteString v)
      throws DirectoryException
  {
    if (passwordPolicy.isAuthPasswordSyntax())
    {
      StringBuilder[] pwComps = AuthPasswordSyntax.decodeAuthPassword(v.toString());
      return DirectoryServer.getAuthPasswordStorageScheme(pwComps[0].toString());
    }
    else
    {
      String[] pwComps = UserPasswordSyntax.decodeUserPassword(v.toString());
      return DirectoryServer.getPasswordStorageScheme(pwComps[0]);
    }
  }
  /**
   * {@inheritDoc}
@@ -2528,7 +2552,14 @@
  }
  /**
   * Get the broken-down components of the given password value.
   *
   * @param  usesAuthPasswordSyntax  true if the value is an authPassword.
   * @param  v  The encoded password value to break down.
   *
   * @return An array of components.
   */
  private StringBuilder[] getPwComponents(boolean usesAuthPasswordSyntax,
      ByteString v) throws DirectoryException
  {
@@ -2863,7 +2894,6 @@
      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))
@@ -3151,8 +3181,10 @@
  /**
   * Updates the password history information for this user by adding all
   * current passwords to it.
   * Updates the password history information for this user by adding one of
   * the passwords to it. It will choose the first password encoded using a
   * secure storage scheme, and will fall back to a password encoded using an
   * insecure storage scheme if necessary.
   */
  public void updatePasswordHistory()
  {
@@ -3162,9 +3194,40 @@
    {
      for (Attribute a : attrList)
      {
        boolean usesAuthPasswordSyntax = passwordPolicy.isAuthPasswordSyntax();
        ByteString insecurePassword = null;
        for (ByteString v : a)
        {
          addPasswordToHistory(v.toString());
          try
          {
            PasswordStorageScheme<?> scheme = getPasswordStorageScheme(v);
            if (scheme.isStorageSchemeSecure())
            {
              addPasswordToHistory(v.toString());
              insecurePassword = null;
              // no need to check any more values for this attribute
              break;
            }
            else if (insecurePassword == null)
            {
              insecurePassword = v;
            }
          }
          catch (DirectoryException e)
          {
            if (logger.isTraceEnabled())
            {
              logger.trace("Encoded password " + v.toString() +
                      " cannot be decoded and cannot be added to history.");
            }
          }
        }
        // If we get here we haven't found a password encoded securely, so we
        // have to use one of the other values.
        if (insecurePassword != null)
        {
          addPasswordToHistory(insecurePassword.toString());
        }
      }
    }
opendj3-server-dev/tests/unit-tests-testng/src/server/org/opends/server/core/PasswordPolicyTestCase.java
@@ -35,20 +35,20 @@
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.ByteString;
import org.opends.server.TestCaseUtils;
import org.opends.server.admin.server.AdminTestCaseUtils;
import org.opends.server.admin.std.meta.PasswordPolicyCfgDefn;
import org.opends.server.admin.std.server.PasswordPolicyCfg;
import org.opends.server.api.PasswordStorageScheme;
import org.opends.server.config.ConfigEntry;
import org.forgerock.opendj.config.server.ConfigException;
import org.opends.server.schema.UserPasswordSyntax;
import org.opends.server.tools.LDAPModify;
import org.opends.server.types.AttributeType;
import org.opends.server.types.DN;
import org.opends.server.types.Entry;
import org.opends.server.types.InitializationException;
import org.opends.server.types.*;
import org.opends.server.util.TimeThread;
import static org.assertj.core.api.Assertions.*;
import static org.testng.Assert.*;
@@ -4277,6 +4277,170 @@
  /**
   * Sleep until we can be sure that the time thread has been updated.
   *
   * Otherwise, password changes can all appear to be in the same
   * millisecond and really weird things start to happen because of the way
   * that we handle conflicts in password history timestamps.  In short, if
   * the history is already at the maximum count and all the previous
   * changes occurred in the same millisecond as the new change, then it's
   * possible for a new change to come with a timestamp that is before the
   * timestamps of all the existing values.
   *
   * @throws InterruptedException
   */
  private void advanceTimeThread() throws InterruptedException
  {
    long timeThreadCurrentTime = TimeThread.getTime();
    while (timeThreadCurrentTime == TimeThread.getTime())
    {
      Thread.sleep(10);
    }
  }
  /**
   * Check there are 3 securely stored passwords in the entry's password history.
   *
   * @param  dn  The entry to check.
   *
   * @return The password history.
   */
  private Attribute checkPasswordHistory(String dn)
      throws DirectoryException
  {
    Entry entry = DirectoryServer.getEntry(DN.valueOf(dn));
    assertNotNull(entry);
    AttributeType pwdHistory = DirectoryServer.getAttributeType("pwdhistory");
    assertNotNull(pwdHistory);
    Attribute historyAttr = entry.getExactAttribute(pwdHistory, null);
    assertNotNull(historyAttr);
    assertThat(historyAttr).hasSize(3);
    for (ByteString v : historyAttr)
    {
      String[] history = v.toString().split("#");
      assertEquals(history.length, 3);
      String[] pwComps = UserPasswordSyntax.decodeUserPassword(history[2]);
      PasswordStorageScheme<?> scheme = DirectoryServer.getPasswordStorageScheme(pwComps[0]);
      assertTrue(scheme.isStorageSchemeSecure());
    }
    return historyAttr;
  }
  /**
   * Use multiple storage schemes in the policy. Verify that history only
   * contains one secure version of each password, is truncated correctly,
   * and continues to change after reaching maximum.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test()
  public void testHistoryWithMultipleSchemes()
         throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);
    TestCaseUtils.dsconfig(
            "set-password-policy-prop",
            "--policy-name", "Default Password Policy",
            "--set", "password-history-count:3",
            "--set", "default-password-storage-scheme:Base64",
            "--set", "default-password-storage-scheme:Salted SHA-256");
    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");
    try
    {
      // 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");
      String[] 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);
      advanceTimeThread();
      Attribute historyAfterThreeMods = checkPasswordHistory("uid=test.user,o=test");
      newPWsPath = TestCaseUtils.createTempFile(
              "dn: uid=test.user,o=test",
              "changetype: modify",
              "replace: userPassword",
              "userPassword: newPassword4");
      args = new String[]
              {
                      "-h", "127.0.0.1",
                      "-p", String.valueOf(TestCaseUtils.getServerLdapPort()),
                      "-D", "uid=test.user,o=test",
                      "-w", "newPassword3",
                      "-f", newPWsPath
              };
      assertEquals(LDAPModify.mainModify(args, false, System.out, System.err),
              0);
      advanceTimeThread();
      Attribute historyAfterFourMods = checkPasswordHistory("uid=test.user,o=test");
      // check history is now different (OPENDJ-1497)
      assertFalse(historyAfterThreeMods.equals(historyAfterFourMods));
    }
    finally
    {
      TestCaseUtils.dsconfig(
              "set-password-policy-prop",
              "--policy-name", "Default Password Policy",
              "--set", "default-password-storage-scheme:Salted SHA-1",
              "--set", "password-history-count:0");
    }
  }
  /**
   * Tests the Directory Server's password history maintenance capabilities
   * using only the password history count configuration option.
   *
@@ -4362,19 +4526,7 @@
                   0);
      // Sleep until we can be sure that the time thread has been updated.
      // Otherwise, the password changes can all appear to be in the same
      // millisecond and really weird things start to happen because of the way
      // that we handle conflicts in password history timestamps.  In short, if
      // the history is already at the maximum count and all the previous
      // changes occurred in the same millisecond as the new change, then it's
      // possible for a new change to come with a timestamp that is before the
      // timestamps of all the existing values.
      long timeThreadCurrentTime = TimeThread.getTime();
      while (timeThreadCurrentTime == TimeThread.getTime())
      {
        Thread.sleep(10);
      }
      advanceTimeThread();
      // Make sure that we still can't use the original password.
@@ -4417,12 +4569,7 @@
                   0);
      // Sleep again to ensure that the time thread is updated.
      timeThreadCurrentTime = TimeThread.getTime();
      while (timeThreadCurrentTime == TimeThread.getTime())
      {
        Thread.sleep(10);
      }
      advanceTimeThread();
      // Make sure that we can't use the second new password.
@@ -4445,12 +4592,7 @@
                  0);
      // Sleep again to ensure that the time thread is updated.
      timeThreadCurrentTime = TimeThread.getTime();
      while (timeThreadCurrentTime == TimeThread.getTime())
      {
        Thread.sleep(10);
      }
      advanceTimeThread();
      // Reduce the password history count from 3 to 2 and verify that we can