From 17eaa1ec294407aedf295965be72854d5a570179 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.

---
 opends/src/server/org/opends/server/extensions/PasswordModifyExtendedOperation.java |  513 +++++++++++++++++++++++++++++++++++++++++++++++++-------
 1 files changed, 449 insertions(+), 64 deletions(-)

diff --git a/opends/src/server/org/opends/server/extensions/PasswordModifyExtendedOperation.java b/opends/src/server/org/opends/server/extensions/PasswordModifyExtendedOperation.java
index 2a3632c..4ff6127 100644
--- a/opends/src/server/org/opends/server/extensions/PasswordModifyExtendedOperation.java
+++ b/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()));

--
Gitblit v1.10.0