From 11fd34d7f9c620e369fbccaa7c310b5d04a8747e Mon Sep 17 00:00:00 2001
From: neil_a_wilson <neil_a_wilson@localhost>
Date: Fri, 04 Aug 2006 05:28:15 +0000
Subject: [PATCH] Update the password modify extended operation so that it includes all appropriate password policy processing.

---
 opendj-sdk/opends/src/server/org/opends/server/core/PasswordPolicyState.java                   |  280 +++++++++++++++++
 opendj-sdk/opends/src/server/org/opends/server/extensions/PasswordModifyExtendedOperation.java |  513 ++++++++++++++++++++++++++++----
 opendj-sdk/opends/src/server/org/opends/server/messages/ExtensionsMessages.java                |  158 +++++++++
 3 files changed, 879 insertions(+), 72 deletions(-)

diff --git a/opendj-sdk/opends/src/server/org/opends/server/core/PasswordPolicyState.java b/opendj-sdk/opends/src/server/org/opends/server/core/PasswordPolicyState.java
index a164336..d907102 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/core/PasswordPolicyState.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/core/PasswordPolicyState.java
@@ -180,6 +180,9 @@
   // The password policy with which the account is associated.
   private PasswordPolicy passwordPolicy;
 
+  // The string representation of the current time.
+  private String currentGeneralizedTime;
+
   // The string representation of the user's DN.
   private String userDNString;
 
@@ -213,6 +216,7 @@
 
     userDNString           = userEntry.getDN().toString();
     passwordPolicy         = getPasswordPolicyInternal();
+    currentGeneralizedTime = TimeThread.getGeneralizedTime();
     currentTime            = TimeThread.getTime();
     modifications          = new LinkedList<Modification>();
     isDisabled             = ConditionResult.UNDEFINED;
@@ -770,6 +774,27 @@
 
 
   /**
+   * Retrieves the set of values for the password attribute from the user entry.
+   *
+   * @return  The set of values for the password attribute from the user entry.
+   */
+  public LinkedHashSet<AttributeValue> getPasswordValues()
+  {
+    assert debugEnter(CLASS_NAME, "getPasswordValues");
+
+    List<Attribute> attrList =
+         userEntry.getAttribute(passwordPolicy.getPasswordAttribute());
+    for (Attribute a : attrList)
+    {
+      return a.getValues();
+    }
+
+    return new LinkedHashSet<AttributeValue>(0);
+  }
+
+
+
+  /**
    * Indicates whether the associated password policy requires that
    * authentication be performed in a secure manner.
    *
@@ -854,6 +879,23 @@
 
 
   /**
+   * Indicates whether users will be required to provide their current password
+   * when choosing a new one.
+   *
+   * @return  <CODE>true</CODE> if users will be required to provide their
+   *          current password when choosing a new one, or <CODE>false</CODE>
+   *          if not.
+   */
+  public boolean requireCurrentPassword()
+  {
+    assert debugEnter(CLASS_NAME, "requireCurrentPassword");
+
+    return passwordPolicy.requireCurrentPassword();
+  }
+
+
+
+  /**
    * Indicates whether administrative password resets should be allowed to
    * bypass validity checks for the new password.
    *
@@ -937,6 +979,36 @@
 
 
   /**
+   * Retrieves time that this password policy state object was created.
+   *
+   * @return  The time that this password policy state object was created.
+   */
+  public long getCurrentTime()
+  {
+    assert debugEnter(CLASS_NAME, "getCurrentTime");
+
+    return currentTime;
+  }
+
+
+
+  /**
+   * Retrieves the generalized time representation of the time that this
+   * password policy state object was created.
+   *
+   * @return  The generalized time representation of the time that this
+   *          password policy state object was created.
+   */
+  public String getCurrentGeneralizedTime()
+  {
+    assert debugEnter(CLASS_NAME, "getCurrentGeneralizedTime");
+
+    return currentGeneralizedTime;
+  }
+
+
+
+  /**
    * Sets a new value for the password changed time equal to the current time.
    */
   public void setPasswordChangedTime()
@@ -1140,7 +1212,7 @@
       }
       else
       {
-        modifications.add(new Modification(ModificationType.DELETE,
+        modifications.add(new Modification(ModificationType.REPLACE,
                                            new Attribute(type)));
       }
     }
@@ -1384,7 +1456,7 @@
         }
         else
         {
-          modifications.add(new Modification(ModificationType.DELETE,
+          modifications.add(new Modification(ModificationType.REPLACE,
                                              new Attribute(type)));
         }
       }
@@ -1514,7 +1586,7 @@
     }
     else
     {
-      modifications.add(new Modification(ModificationType.DELETE,
+      modifications.add(new Modification(ModificationType.REPLACE,
                                          new Attribute(type)));
     }
   }
@@ -1664,7 +1736,7 @@
         }
         else
         {
-          modifications.add(new Modification(ModificationType.DELETE,
+          modifications.add(new Modification(ModificationType.REPLACE,
                                              new Attribute(type)));
         }
 
@@ -1800,7 +1872,7 @@
     }
     else
     {
-      modifications.add(new Modification(ModificationType.DELETE,
+      modifications.add(new Modification(ModificationType.REPLACE,
                                          new Attribute(type)));
     }
   }
@@ -2336,7 +2408,7 @@
       }
       else
       {
-        modifications.add(new Modification(ModificationType.DELETE,
+        modifications.add(new Modification(ModificationType.REPLACE,
                                            new Attribute(type)));
       }
     }
@@ -2625,6 +2697,87 @@
 
 
   /**
+   * Indicates whether users will be allowed to change their passwords if they
+   * are expired.
+   *
+   * @return  <CODE>true</CODE> if users will be allowed to change their
+   *          passwords if they are expired, or <CODE>false</CODE> if not.
+   */
+  public boolean allowExpiredPasswordChanges()
+  {
+    assert debugEnter(CLASS_NAME, "allowExpiredPasswordChanges");
+
+    return passwordPolicy.allowExpiredPasswordChanges();
+  }
+
+
+
+  /**
+   * Indicates whether the user's last password change was within the minimum
+   * password age.
+   *
+   * @return  <CODE>true</CODE> if the password minimum age is nonzero, the
+   *          account is not in force-change mode, and the last password change
+   *          was within the minimum age, or <CODE>false</CODE> otherwise.
+   */
+  public boolean isWithinMinimumAge()
+  {
+    assert debugEnter(CLASS_NAME, "isWithinMinimumAge");
+
+    int minAge = passwordPolicy.getMinimumPasswordAge();
+    if (minAge <= 0)
+    {
+      // There is no minimum age, so the user isn't in it.
+      if (debug)
+      {
+        debugMessage(DebugLogCategory.PASSWORD_POLICY, DebugLogSeverity.INFO,
+                     CLASS_NAME, "isWithinMinimumAge",
+                     "Returning false because there is no minimum age.");
+      }
+
+      return false;
+    }
+    else if ((passwordChangedTime + (minAge*1000)) < currentTime)
+    {
+      // It's been long enough since the user changed their password.
+      if (debug)
+      {
+        debugMessage(DebugLogCategory.PASSWORD_POLICY, DebugLogSeverity.INFO,
+                     CLASS_NAME, "isWithinMinimumAge",
+                     "Returning false because the minimum age has expired.");
+      }
+
+      return false;
+    }
+    else if (mustChangePassword())
+    {
+      // The user is in a must-change mode, so the minimum age doesn't apply.
+      if (debug)
+      {
+        debugMessage(DebugLogCategory.PASSWORD_POLICY, DebugLogSeverity.INFO,
+                     CLASS_NAME, "isWithinMinimumAge",
+                     "Returning false because the account is in a " +
+                     "must-change state.");
+      }
+
+      return false;
+    }
+    else
+    {
+      // The user is within the minimum age.
+      if (debug)
+      {
+        debugMessage(DebugLogCategory.PASSWORD_POLICY, DebugLogSeverity.WARNING,
+                     CLASS_NAME, "isWithinMinimumAge", "Returning true.");
+      }
+
+      return true;
+    }
+  }
+
+
+
+  /**
    * Indicates whether the user may use a grace login if the password is expired
    * and there is at least one grace login remaining.  Note that this does not
    * check to see if the user's password is expired, does not verify that there
@@ -2949,6 +3102,35 @@
 
 
   /**
+   * Updates the user entry to clear the warned time.
+   */
+  public void clearWarnedTime()
+  {
+    assert debugEnter(CLASS_NAME, "clearWarnedTime");
+
+    AttributeType type =
+         DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_WARNED_TIME, true);
+    if (updateEntry)
+    {
+      userEntry.removeAttribute(type);
+    }
+    else
+    {
+      Attribute a = new Attribute(type);
+      modifications.add(new Modification(ModificationType.REPLACE, a));
+    }
+
+    if (debug)
+    {
+      debugMessage(DebugLogCategory.PASSWORD_POLICY, DebugLogSeverity.INFO,
+                   CLASS_NAME, "clearWarnedTime",
+                   "Cleared the warned time for user " + userDNString);
+    }
+  }
+
+
+
+  /**
    * Retrieves the maximum number of grace logins that the user will be allowed
    * according to the associated password policy.
    *
@@ -3011,7 +3193,7 @@
         }
         else
         {
-          modifications.add(new Modification(ModificationType.DELETE,
+          modifications.add(new Modification(ModificationType.REPLACE,
                                              new Attribute(type)));
         }
       }
@@ -3159,7 +3341,7 @@
     }
     else
     {
-      modifications.add(new Modification(ModificationType.DELETE,
+      modifications.add(new Modification(ModificationType.REPLACE,
                                          new Attribute(type)));
     }
   }
@@ -3336,6 +3518,88 @@
 
 
   /**
+   * Indicates whether the user's password is stored using the auth password
+   * syntax or the user password syntax.
+   *
+   * @return  <CODE>true</CODE> if the user's password is stored using the auth
+   *          password syntax, or <CODE>false</CODE> if it is stored using the
+   *          user password syntax.
+   */
+  public boolean usesAuthPasswordSyntax()
+  {
+    assert debugEnter(CLASS_NAME, "usesAuthPasswordSyntax");
+
+    return passwordPolicy.usesAuthPasswordSyntax();
+  }
+
+
+
+  /**
+   * Indicates whether the provided password value is pre-encoded.
+   *
+   * @param  passwordValue  The value for which to make the determination.
+   *
+   * @return  <CODE>true</CODE> if the provided password value is pre-encoded,
+   *          or <CODE>false</CODE> if it is not.
+   */
+  public boolean passwordIsPreEncoded(ByteString passwordValue)
+  {
+    assert debugEnter(CLASS_NAME, "isPreEncoded", "ByteString");
+
+    if (passwordPolicy.usesAuthPasswordSyntax())
+    {
+      return AuthPasswordSyntax.isEncoded(passwordValue);
+    }
+    else
+    {
+      return UserPasswordSyntax.isEncoded(passwordValue);
+    }
+  }
+
+
+
+  /**
+   * Encodes the provided password using the default storage schemes (using the
+   * appropriate syntax for the password attribute).
+   *
+   * @param  password  The password to be encoded.
+   *
+   * @return  The password encoded using the default schemes.
+   *
+   * @throws  DirectoryException  If a problem occurs while attempting to encode
+   *                              the password.
+   */
+  public List<ByteString> encodePassword(ByteString password)
+         throws DirectoryException
+  {
+    assert debugEnter(CLASS_NAME, "encodePassword", "ByteString");
+
+    List<PasswordStorageScheme> schemes =
+         passwordPolicy.getDefaultStorageSchemes();
+    List<ByteString> encodedPasswords =
+         new ArrayList<ByteString>(schemes.size());
+
+    if (passwordPolicy.usesAuthPasswordSyntax())
+    {
+      for (PasswordStorageScheme s : schemes)
+      {
+        encodedPasswords.add(s.encodeAuthPassword(password));
+      }
+    }
+    else
+    {
+      for (PasswordStorageScheme s : schemes)
+      {
+        encodedPasswords.add(s.encodePasswordWithScheme(password));
+      }
+    }
+
+    return encodedPasswords;
+  }
+
+
+
+  /**
    * Indicates whether the provided password appears to be acceptable according
    * to the password validators.
    *
diff --git a/opendj-sdk/opends/src/server/org/opends/server/extensions/PasswordModifyExtendedOperation.java b/opendj-sdk/opends/src/server/org/opends/server/extensions/PasswordModifyExtendedOperation.java
index 2a3632c..4ff6127 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/extensions/PasswordModifyExtendedOperation.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/extensions/PasswordModifyExtendedOperation.java
@@ -29,12 +29,14 @@
 
 
 import java.util.ArrayList;
+import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.concurrent.locks.Lock;
 
 import org.opends.server.api.ClientConnection;
 import org.opends.server.api.ExtendedOperationHandler;
+import org.opends.server.api.PasswordStorageScheme;
 import org.opends.server.config.ConfigEntry;
 import org.opends.server.config.ConfigException;
 import org.opends.server.core.DirectoryException;
@@ -43,15 +45,16 @@
 import org.opends.server.core.InitializationException;
 import org.opends.server.core.LockManager;
 import org.opends.server.core.ModifyOperation;
+import org.opends.server.core.PasswordPolicyState;
 import org.opends.server.protocols.asn1.ASN1Element;
 import org.opends.server.protocols.asn1.ASN1Exception;
 import org.opends.server.protocols.asn1.ASN1OctetString;
 import org.opends.server.protocols.asn1.ASN1Sequence;
 import org.opends.server.protocols.internal.InternalClientConnection;
 import org.opends.server.protocols.internal.InternalSearchOperation;
-import org.opends.server.protocols.ldap.LDAPAttribute;
 import org.opends.server.protocols.ldap.LDAPFilter;
-import org.opends.server.protocols.ldap.LDAPModification;
+import org.opends.server.schema.AuthPasswordSyntax;
+import org.opends.server.schema.UserPasswordSyntax;
 import org.opends.server.types.Attribute;
 import org.opends.server.types.AttributeType;
 import org.opends.server.types.AttributeValue;
@@ -59,11 +62,13 @@
 import org.opends.server.types.ByteString;
 import org.opends.server.types.DN;
 import org.opends.server.types.Entry;
+import org.opends.server.types.Modification;
 import org.opends.server.types.ModificationType;
 import org.opends.server.types.ResultCode;
 import org.opends.server.types.SearchResultEntry;
 import org.opends.server.types.SearchScope;
 
+import static org.opends.server.config.ConfigConstants.*;
 import static org.opends.server.extensions.ExtensionsConstants.*;
 import static org.opends.server.loggers.Debug.*;
 import static org.opends.server.messages.ExtensionsMessages.*;
@@ -126,12 +131,6 @@
     assert debugEnter(CLASS_NAME, "initializeExtendedOperationHandler",
                       String.valueOf(configEntry));
 
-    // NYI -- parse the config entry for any settings that might be defined
-    // This can include:
-    // - Whether to require the old password
-    // - Whether to allow automatic generation of a new password
-    // - The class name for an algorithm to generate new passwords
-
     DirectoryServer.registerSupportedExtension(OID_PASSWORD_MODIFY_REQUEST,
                                                this);
   }
@@ -163,9 +162,9 @@
 
     // Initialize the variables associated with components that may be included
     // in the request.
-    ASN1OctetString userIdentity = null;
-    ASN1OctetString oldPassword  = null;
-    ASN1OctetString newPassword  = null;
+    ByteString userIdentity = null;
+    ByteString oldPassword  = null;
+    ByteString newPassword  = null;
 
 
     // Parse the encoded request, if there is one.
@@ -246,14 +245,6 @@
         }
 
 
-        // If the user is connected over an insecure channel, then determine
-        // whether we should attempt to proceed.
-        if (! clientConnection.isSecure())
-        {
-          // NYI
-        }
-
-
         // Retrieve a write lock on that user's entry.
         userDN = requestorDN;
 
@@ -343,75 +334,467 @@
       }
 
 
-      // At this point, we should have the user entry.  If a current password
-      // was provided, then validate it.  If not, then see if that's OK.
-      AttributeType pwType = DirectoryServer.getAttributeType("userpassword");
-      if (pwType == null)
+      // At this point, we should have the user entry.  Get the associated
+      // password policy.
+      PasswordPolicyState pwPolicyState;
+      try
       {
-        pwType = DirectoryServer.getDefaultAttributeType("userPassword");
+        pwPolicyState = new PasswordPolicyState(userEntry, false, false);
+      }
+      catch (DirectoryException de)
+      {
+        assert debugException(CLASS_NAME, "processExtendedOperation", de);
+
+        operation.setResultCode(DirectoryServer.getServerErrorResultCode());
+
+        int msgID = MSGID_EXTOP_PASSMOD_CANNOT_GET_PW_POLICY;
+        operation.appendErrorMessage(getMessage(msgID, String.valueOf(userDN),
+                                                de.getErrorMessage()));
+        return;
       }
 
+
+      // Determine whether the user is changing his own password or if it's an
+      // administrative reset.
+      boolean selfChange = ((userIdentity == null) ||
+                            userDN.equals(requestorDN));
+
+
+      // If the current password was provided, then we'll need to verify whether
+      // it was correct.  If it wasn't provided but this is a self change, then
+      // make sure that's OK.
       if (oldPassword == null)
       {
-        // NYI -- Confirm that this is allowed.
+        if (selfChange && pwPolicyState.requireCurrentPassword())
+        {
+          operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+
+          int msgID = MSGID_EXTOP_PASSMOD_REQUIRE_CURRENT_PW;
+          operation.appendErrorMessage(getMessage(msgID));
+          return;
+        }
       }
       else
       {
-        // FIXME -- Use a more generic check to determine the correct attribute
-        List<Attribute> pwAttrList = userEntry.getAttribute(pwType);
-        if ((pwAttrList == null) || pwAttrList.isEmpty())
+        if (pwPolicyState.requireSecureAuthentication() &&
+            (! operation.getClientConnection().isSecure()))
         {
-          // There were no existing passwords, so the validation will fail.
           operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
 
-          int msgID = MSGID_EXTOP_PASSMOD_INVALID_OLD_PASSWORD;
-          operation.appendErrorMessage(getMessage(msgID));
+          int msgID = MSGID_EXTOP_PASSMOD_SECURE_AUTH_REQUIRED;
+          operation.appendAdditionalLogMessage(getMessage(msgID));
           return;
         }
 
-        AttributeValue matchValue = new AttributeValue(pwType, oldPassword);
-
-        boolean matchFound = false;
-        for (Attribute a : pwAttrList)
+        if (! pwPolicyState.passwordMatches(oldPassword))
         {
-          if (a.hasValue(matchValue))
+          operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
+
+          int msgID = MSGID_EXTOP_PASSMOD_INVALID_OLD_PASSWORD;
+          operation.appendAdditionalLogMessage(getMessage(msgID));
+          return;
+        }
+      }
+
+
+      // If it is a self password change and we don't allow that, then reject
+      // the request.
+      if (selfChange && (! pwPolicyState.allowUserPasswordChanges()))
+      {
+        if (oldPassword == null)
+        {
+          operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+
+          int msgID = MSGID_EXTOP_PASSMOD_USER_PW_CHANGES_NOT_ALLOWED;
+          operation.appendErrorMessage(getMessage(msgID));
+        }
+        else
+        {
+          operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
+
+          int msgID = MSGID_EXTOP_PASSMOD_USER_PW_CHANGES_NOT_ALLOWED;
+          operation.appendAdditionalLogMessage(getMessage(msgID));
+        }
+
+        return;
+      }
+
+
+      // If we require secure password changes and the connection isn't secure,
+      // then reject the request.
+      if (pwPolicyState.requireSecurePasswordChanges() &&
+          (! operation.getClientConnection().isSecure()))
+      {
+        if (oldPassword == null)
+        {
+          operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+
+          int msgID = MSGID_EXTOP_PASSMOD_SECURE_CHANGES_REQUIRED;
+          operation.appendErrorMessage(getMessage(msgID));
+        }
+        else
+        {
+          operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
+
+          int msgID = MSGID_EXTOP_PASSMOD_SECURE_CHANGES_REQUIRED;
+          operation.appendAdditionalLogMessage(getMessage(msgID));
+        }
+
+        return;
+      }
+
+
+      // If it's a self-change request and the user is within the minimum age,
+      // then reject it.
+      if (selfChange && pwPolicyState.isWithinMinimumAge())
+      {
+        if (oldPassword == null)
+        {
+          operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+
+          int msgID = MSGID_EXTOP_PASSMOD_IN_MIN_AGE;
+          operation.appendErrorMessage(getMessage(msgID));
+        }
+        else
+        {
+          operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
+
+          int msgID = MSGID_EXTOP_PASSMOD_IN_MIN_AGE;
+          operation.appendAdditionalLogMessage(getMessage(msgID));
+        }
+
+        return;
+      }
+
+
+      // If the user's password is expired and it's a self-change request, then
+      // see if that's OK.
+      if ((selfChange && pwPolicyState.isPasswordExpired() &&
+          (! pwPolicyState.allowExpiredPasswordChanges())))
+      {
+        if (oldPassword == null)
+        {
+          operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+
+          int msgID = MSGID_EXTOP_PASSMOD_PASSWORD_IS_EXPIRED;
+          operation.appendErrorMessage(getMessage(msgID));
+        }
+        else
+        {
+          operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
+
+          int msgID = MSGID_EXTOP_PASSMOD_PASSWORD_IS_EXPIRED;
+          operation.appendAdditionalLogMessage(getMessage(msgID));
+        }
+
+        return;
+      }
+
+
+
+      // If the a new password was provided, then peform any appropriate
+      // validation on it.  If not, then see if we can generate one.
+      boolean generatedPassword = false;
+      boolean isPreEncoded      = false;
+      if (newPassword == null)
+      {
+        try
+        {
+          newPassword = pwPolicyState.generatePassword();
+          if (newPassword == null)
           {
-            matchFound = true;
-            break;
+            if (oldPassword == null)
+            {
+              operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+
+              int msgID = MSGID_EXTOP_PASSMOD_NO_PW_GENERATOR;
+              operation.appendErrorMessage(getMessage(msgID));
+            }
+            else
+            {
+              operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
+
+              int msgID = MSGID_EXTOP_PASSMOD_NO_PW_GENERATOR;
+              operation.appendAdditionalLogMessage(getMessage(msgID));
+            }
+
+            return;
+          }
+          else
+          {
+            generatedPassword = true;
+          }
+        }
+        catch (DirectoryException de)
+        {
+          assert debugException(CLASS_NAME, "processExtendedOperation", de);
+
+          if (oldPassword == null)
+          {
+            operation.setResultCode(de.getResultCode());
+
+            int msgID = MSGID_EXTOP_PASSMOD_CANNOT_GENERATE_PW;
+            operation.appendErrorMessage(getMessage(msgID,
+                                                    de.getErrorMessage()));
+          }
+          else
+          {
+            operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
+
+            int msgID = MSGID_EXTOP_PASSMOD_CANNOT_GENERATE_PW;
+            operation.appendAdditionalLogMessage(getMessage(msgID,
+                                                      de.getErrorMessage()));
+          }
+
+          return;
+        }
+      }
+      else
+      {
+        if (pwPolicyState.passwordIsPreEncoded(newPassword))
+        {
+          // The password modify extended operation isn't intended to be invoked
+          // by an internal operation or during synchronization, so we don't
+          // need to check for those cases.
+          isPreEncoded = true;
+          if (! pwPolicyState.allowPreEncodedPasswords())
+          {
+            if (oldPassword == null)
+            {
+              operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+
+              int msgID = MSGID_EXTOP_PASSMOD_PRE_ENCODED_NOT_ALLOWED;
+              operation.appendErrorMessage(getMessage(msgID));
+            }
+            else
+            {
+              operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
+
+              int msgID = MSGID_EXTOP_PASSMOD_PRE_ENCODED_NOT_ALLOWED;
+              operation.appendAdditionalLogMessage(getMessage(msgID));
+            }
+
+            return;
+          }
+        }
+        else
+        {
+          if (selfChange || (! pwPolicyState.skipValidationForAdministrators()))
+          {
+            StringBuilder invalidReason = new StringBuilder();
+            if (! pwPolicyState.passwordIsAcceptable(operation, userEntry,
+                                                     newPassword,
+                                                     invalidReason))
+            {
+              if (oldPassword == null)
+              {
+                operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
+
+                int msgID = MSGID_EXTOP_PASSMOD_UNACCEPTABLE_PW;
+                operation.appendErrorMessage(getMessage(msgID,
+                               String.valueOf(invalidReason)));
+              }
+              else
+              {
+                operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
+
+                int msgID = MSGID_EXTOP_PASSMOD_UNACCEPTABLE_PW;
+                operation.appendAdditionalLogMessage(getMessage(msgID,
+                               String.valueOf(invalidReason)));
+              }
+
+              return;
+            }
+          }
+        }
+      }
+
+
+      // Get the encoded forms of the new password.
+      List<ByteString> encodedPasswords;
+      if (isPreEncoded)
+      {
+        encodedPasswords = new ArrayList<ByteString>(1);
+        encodedPasswords.add(newPassword);
+      }
+      else
+      {
+        try
+        {
+          encodedPasswords = pwPolicyState.encodePassword(newPassword);
+        }
+        catch (DirectoryException de)
+        {
+          assert debugException(CLASS_NAME, "processExtendedOperation", de);
+
+          if (oldPassword == null)
+          {
+            operation.setResultCode(de.getResultCode());
+
+            int msgID = MSGID_EXTOP_PASSMOD_CANNOT_ENCODE_PASSWORD;
+            operation.appendErrorMessage(getMessage(msgID,
+                                                    de.getErrorMessage()));
+          }
+          else
+          {
+            operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
+
+            int msgID = MSGID_EXTOP_PASSMOD_CANNOT_ENCODE_PASSWORD;
+            operation.appendAdditionalLogMessage(getMessage(msgID,
+                                                      de.getErrorMessage()));
+          }
+
+          return;
+        }
+      }
+
+
+      // If the current password was provided, then remove all matching values
+      // from the user's entry and replace them with the new password.
+      // Otherwise replace all password values.
+      AttributeType attrType = pwPolicyState.getPasswordAttribute();
+      List<Modification> modList = new ArrayList<Modification>();
+      if (oldPassword != null)
+      {
+        // Remove all existing encoded values that match the old password.
+        LinkedHashSet<AttributeValue> existingValues =
+             pwPolicyState.getPasswordValues();
+        LinkedHashSet<AttributeValue> deleteValues =
+             new LinkedHashSet<AttributeValue>(existingValues.size());
+        if (pwPolicyState.usesAuthPasswordSyntax())
+        {
+          for (AttributeValue v : existingValues)
+          {
+            try
+            {
+              StringBuilder[] components =
+                   AuthPasswordSyntax.decodeAuthPassword(v.getStringValue());
+              PasswordStorageScheme scheme =
+                   DirectoryServer.getAuthPasswordStorageScheme(
+                        components[0].toString());
+              if (scheme == null)
+              {
+                // The password is encoded using an unknown scheme.  Remove it
+                // from the user's entry.
+                deleteValues.add(v);
+              }
+              else
+              {
+                if (scheme.authPasswordMatches(oldPassword,
+                                               components[1].toString(),
+                                               components[2].toString()))
+                {
+                  deleteValues.add(v);
+                }
+              }
+            }
+            catch (DirectoryException de)
+            {
+              assert debugException(CLASS_NAME, "processExtendedOperation", de);
+
+              // We couldn't decode the provided password value, so remove it
+              // from the user's entry.
+              deleteValues.add(v);
+            }
+          }
+        }
+        else
+        {
+          for (AttributeValue v : existingValues)
+          {
+            try
+            {
+              String[] components =
+                   UserPasswordSyntax.decodeUserPassword(v.getStringValue());
+              PasswordStorageScheme scheme =
+                   DirectoryServer.getPasswordStorageScheme(
+                        toLowerCase(components[0].toString()));
+              if (scheme == null)
+              {
+                // The password is encoded using an unknown scheme.  Remove it
+                // from the user's entry.
+                deleteValues.add(v);
+              }
+              else
+              {
+                if (scheme.passwordMatches(oldPassword,
+                                           new ASN1OctetString(components[1])))
+                {
+                  deleteValues.add(v);
+                }
+              }
+            }
+            catch (DirectoryException de)
+            {
+              assert debugException(CLASS_NAME, "processExtendedOperation", de);
+
+              // We couldn't decode the provided password value, so remove it
+              // from the user's entry.
+              deleteValues.add(v);
+            }
           }
         }
 
-        if (! matchFound)
+        Attribute deleteAttr = new Attribute(attrType, attrType.getNameOrOID(),
+                                             deleteValues);
+        modList.add(new Modification(ModificationType.DELETE, deleteAttr));
+
+
+        // Add the new encoded values.
+        LinkedHashSet<AttributeValue> addValues =
+             new LinkedHashSet<AttributeValue>(encodedPasswords.size());
+        for (ByteString s : encodedPasswords)
         {
-          // None of the password values matched what the user provided.
-          operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
-
-          int msgID = MSGID_EXTOP_PASSMOD_INVALID_OLD_PASSWORD;
-          operation.appendErrorMessage(getMessage(msgID));
-          return;
+          addValues.add(new AttributeValue(attrType, s));
         }
+
+        Attribute addAttr = new Attribute(attrType, attrType.getNameOrOID(),
+                                          addValues);
+        modList.add(new Modification(ModificationType.ADD, addAttr));
       }
-
-
-      // See if a new password was provided.  If not, then generate one.
-      boolean generatedPassword = false;
-      if (newPassword == null)
+      else
       {
-        // FIXME -- use an extensible algorithm for generating the new password.
-        newPassword = new ASN1OctetString("newpassword");
-        generatedPassword = true;
+        LinkedHashSet<AttributeValue> replaceValues =
+             new LinkedHashSet<AttributeValue>(encodedPasswords.size());
+        for (ByteString s : encodedPasswords)
+        {
+          replaceValues.add(new AttributeValue(attrType, s));
+        }
+
+        Attribute addAttr = new Attribute(attrType, attrType.getNameOrOID(),
+                                          replaceValues);
+        modList.add(new Modification(ModificationType.REPLACE, addAttr));
       }
 
-      ArrayList<ASN1OctetString> newPWValues =
-           new ArrayList<ASN1OctetString>(1);
-      newPWValues.add(newPassword);
+
+      // Update the password changed time for the user entry.
+      pwPolicyState.setPasswordChangedTime();
 
 
+      // If the password was changed by an end user, then clear any reset flag
+      // that might exist.  If the password was changed by an administrator,
+      // then see if we need to set the reset flag.
+      if (selfChange)
+      {
+        pwPolicyState.setMustChangePassword(false);
+      }
+      else
+      {
+        pwPolicyState.setMustChangePassword(pwPolicyState.forceChangeOnReset());
+      }
 
-      // Create the modification to update the user's password.
-      LDAPAttribute pwAttr = new LDAPAttribute("userPassword", newPWValues);
-      ArrayList<LDAPModification> mods = new ArrayList<LDAPModification>(1);
-      mods.add(new LDAPModification(ModificationType.REPLACE, pwAttr));
+
+      // Clear any record of grace logins, auth failures, and expiration
+      // warnings.
+      pwPolicyState.clearAuthFailureTimes();
+      pwPolicyState.clearFailureLockout();
+      pwPolicyState.clearGraceLoginTimes();
+      pwPolicyState.clearWarnedTime();
+
+
+      // Get the list of modifications from the password policy state and add
+      // them to the existing password modifications.
+      modList.addAll(pwPolicyState.getModifications());
 
 
       // Get an internal connection and use it to perform the modification.
@@ -421,8 +804,7 @@
            InternalClientConnection(authInfo);
 
       ModifyOperation modifyOperation =
-           internalConnection.processModify(
-                new ASN1OctetString(userDN.toString()), mods);
+           internalConnection.processModify(userDN, modList);
       ResultCode resultCode = modifyOperation.getResultCode();
       if (resultCode != resultCode.SUCCESS)
       {
@@ -441,8 +823,11 @@
       if (generatedPassword)
       {
         ArrayList<ASN1Element> valueElements = new ArrayList<ASN1Element>(1);
-        newPassword.setType(TYPE_PASSWORD_MODIFY_GENERATED_PASSWORD);
-        valueElements.add(newPassword);
+
+        ASN1OctetString newPWString =
+             new ASN1OctetString(TYPE_PASSWORD_MODIFY_GENERATED_PASSWORD,
+                                 newPassword.value());
+        valueElements.add(newPWString);
 
         ASN1Sequence valueSequence = new ASN1Sequence(valueElements);
         operation.setResponseValue(new ASN1OctetString(valueSequence.encode()));
diff --git a/opendj-sdk/opends/src/server/org/opends/server/messages/ExtensionsMessages.java b/opendj-sdk/opends/src/server/org/opends/server/messages/ExtensionsMessages.java
index 7ae8714..102b377 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/messages/ExtensionsMessages.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/messages/ExtensionsMessages.java
@@ -3744,6 +3744,128 @@
 
 
   /**
+   * The message ID for the message that will be used if an error occurs while
+   * attempting to retrieve the password policy for the user.  This takes two
+   * arguments, which are the user DN and a message explaining the problem that
+   * occurred.
+   */
+  public static final int MSGID_EXTOP_PASSMOD_CANNOT_GET_PW_POLICY =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 354;
+
+
+
+  /**
+   * The message ID for the message that will be used if a user password change
+   * is rejected because the current password was not provided.  This does not
+   * take any arguments.
+   */
+  public static final int MSGID_EXTOP_PASSMOD_REQUIRE_CURRENT_PW =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 355;
+
+
+
+  /**
+   * The message ID for the message that will be used if a user password change
+   * is rejected because the current password was provided over an insecure
+   * communication channel.  This does not take any arguments.
+   */
+  public static final int MSGID_EXTOP_PASSMOD_SECURE_AUTH_REQUIRED =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 356;
+
+
+
+  /**
+   * The message ID for the message that will be used if a user password change
+   * is rejected because users are not allowed to change their passwords.  This
+   * does not take any arguments.
+   */
+  public static final int MSGID_EXTOP_PASSMOD_USER_PW_CHANGES_NOT_ALLOWED =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 357;
+
+
+
+  /**
+   * The message ID for the message that will be used if a password change is
+   * rejected because the new password was provided over an insecure
+   * communication channel.  This does not take any arguments.
+   */
+  public static final int MSGID_EXTOP_PASSMOD_SECURE_CHANGES_REQUIRED =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 358;
+
+
+
+  /**
+   * The message ID for the message that will be used if a user password change
+   * is rejected because the current password is too young.  This does not take
+   * any arguments.
+   */
+  public static final int MSGID_EXTOP_PASSMOD_IN_MIN_AGE =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 359;
+
+
+
+  /**
+   * The message ID for the message that will be used if a user password change
+   * is rejected because the current password is expired.  This does not take
+   * any arguments.
+   */
+  public static final int MSGID_EXTOP_PASSMOD_PASSWORD_IS_EXPIRED =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 360;
+
+
+
+  /**
+   * The message ID for the message that will be used if a password change is
+   * rejected because no new password was given and there is no password
+   * generator defined.  This does not take any arguments.
+   */
+  public static final int MSGID_EXTOP_PASSMOD_NO_PW_GENERATOR =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 361;
+
+
+
+  /**
+   * The message ID for the message that will be used if an error occurs while
+   * trying to use the password generator to create a new password.  This takes
+   * a single argument, which is a message explaining the problem that occurred.
+   */
+  public static final int MSGID_EXTOP_PASSMOD_CANNOT_GENERATE_PW =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 362;
+
+
+
+  /**
+   * The message ID for the message that will be used if a password change is
+   * rejected because the new password provided was pre-encoded.  This does not
+   * take any arguments.
+   */
+  public static final int MSGID_EXTOP_PASSMOD_PRE_ENCODED_NOT_ALLOWED =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 363;
+
+
+
+  /**
+   * The message ID for the message that will be used if a password change is
+   * rejected because the new password was rejected by a password validator.
+   * This takes a single argument, which is a message explaining the rejection.
+   */
+  public static final int MSGID_EXTOP_PASSMOD_UNACCEPTABLE_PW =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 364;
+
+
+
+  /**
+   * The message ID for the message that will be used if a password change is
+   * rejected because the new password could not be encoded using the default
+   * schemes.  This takes a single argument, which is a message explaining the
+   * problem that occurred.
+   */
+  public static final int MSGID_EXTOP_PASSMOD_CANNOT_ENCODE_PASSWORD =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 365;
+
+
+
+  /**
    * Associates a set of generic messages with the message IDs defined in this
    * class.
    */
@@ -3969,6 +4091,42 @@
                     "The password modify extended operation cannot be " +
                     "processed because the current password provided for the " +
                     "use is invalid.");
+    registerMessage(MSGID_EXTOP_PASSMOD_CANNOT_GET_PW_POLICY,
+                    "An error occurred while attempting to get the " +
+                    "password policy for user %s:  %s.");
+    registerMessage(MSGID_EXTOP_PASSMOD_REQUIRE_CURRENT_PW,
+                    "The current password must be provided for self password " +
+                    "changes.");
+    registerMessage(MSGID_EXTOP_PASSMOD_SECURE_AUTH_REQUIRED,
+                    "Password modify operations that supply the user's " +
+                    "current password must be performed over a secure " +
+                    "communication channel.");
+    registerMessage(MSGID_EXTOP_PASSMOD_USER_PW_CHANGES_NOT_ALLOWED,
+                    "End users are not allowed to change their passwords.");
+    registerMessage(MSGID_EXTOP_PASSMOD_SECURE_CHANGES_REQUIRED,
+                    "Password changes must be performed over a secure " +
+                    "communication channel.");
+    registerMessage(MSGID_EXTOP_PASSMOD_IN_MIN_AGE,
+                    "The password cannot be changed because the previous " +
+                    "password change was too recent.");
+    registerMessage(MSGID_EXTOP_PASSMOD_PASSWORD_IS_EXPIRED,
+                    "The password cannot be changed because it is expired.");
+    registerMessage(MSGID_EXTOP_PASSMOD_NO_PW_GENERATOR,
+                    "No new password was provided, and no password generator " +
+                    "has been defined that may be used to automatically " +
+                    "create a new password.");
+    registerMessage(MSGID_EXTOP_PASSMOD_CANNOT_GENERATE_PW,
+                    "An error occurred while attempting to create a new " +
+                    "password using the password generator:  %s.");
+    registerMessage(MSGID_EXTOP_PASSMOD_PRE_ENCODED_NOT_ALLOWED,
+                    "The password policy does not allow users to supply " +
+                    "pre-encoded passwords.");
+    registerMessage(MSGID_EXTOP_PASSMOD_UNACCEPTABLE_PW,
+                    "The provided new password failed the validation checks " +
+                    "defined in the server:  %s.");
+    registerMessage(MSGID_EXTOP_PASSMOD_CANNOT_ENCODE_PASSWORD,
+                    "Unable to encode the provided password using the " +
+                    "default scheme(s):  %s.");
 
 
     registerMessage(MSGID_NULL_KEYMANAGER_NO_MANAGER,

--
Gitblit v1.10.0