001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2006-2010 Sun Microsystems, Inc.
025 *      Portions Copyright 2011-2015 ForgeRock AS
026 */
027package org.opends.server.extensions;
028
029import static org.opends.messages.CoreMessages.*;
030import static org.opends.messages.ExtensionMessages.*;
031import static org.opends.server.controls.PasswordPolicyErrorType.*;
032import static org.opends.server.extensions.ExtensionsConstants.*;
033import static org.opends.server.protocols.internal.InternalClientConnection.*;
034import static org.opends.server.types.AccountStatusNotificationType.*;
035import static org.opends.server.util.CollectionUtils.*;
036import static org.opends.server.util.ServerConstants.*;
037import static org.opends.server.util.StaticUtils.*;
038
039import java.io.IOException;
040import java.util.*;
041
042import org.forgerock.i18n.LocalizableMessage;
043import org.forgerock.i18n.LocalizableMessageBuilder;
044import org.forgerock.i18n.slf4j.LocalizedLogger;
045import org.forgerock.opendj.config.server.ConfigChangeResult;
046import org.forgerock.opendj.config.server.ConfigException;
047import org.forgerock.opendj.io.ASN1;
048import org.forgerock.opendj.io.ASN1Reader;
049import org.forgerock.opendj.io.ASN1Writer;
050import org.forgerock.opendj.ldap.ByteString;
051import org.forgerock.opendj.ldap.ByteStringBuilder;
052import org.forgerock.opendj.ldap.ModificationType;
053import org.forgerock.opendj.ldap.ResultCode;
054import org.opends.server.admin.server.ConfigurationChangeListener;
055import org.opends.server.admin.std.server.ExtendedOperationHandlerCfg;
056import org.opends.server.admin.std.server.PasswordModifyExtendedOperationHandlerCfg;
057import org.opends.server.api.*;
058import org.opends.server.controls.PasswordPolicyErrorType;
059import org.opends.server.controls.PasswordPolicyResponseControl;
060import org.opends.server.core.DirectoryServer;
061import org.opends.server.core.ExtendedOperation;
062import org.opends.server.core.ModifyOperation;
063import org.opends.server.core.PasswordPolicyState;
064import org.opends.server.protocols.internal.InternalClientConnection;
065import org.opends.server.schema.AuthPasswordSyntax;
066import org.opends.server.schema.UserPasswordSyntax;
067import org.opends.server.types.*;
068import org.opends.server.types.LockManager.DNLock;
069
070/**
071 * This class implements the password modify extended operation defined in RFC
072 * 3062.  It includes support for requiring the user's current password as well
073 * as for generating a new password if none was provided.
074 */
075public class PasswordModifyExtendedOperation
076       extends ExtendedOperationHandler<PasswordModifyExtendedOperationHandlerCfg>
077       implements ConfigurationChangeListener<PasswordModifyExtendedOperationHandlerCfg>
078{
079  // The following attachments may be used by post-op plugins (e.g. Samba) in
080  // order to avoid re-decoding the request parameters and also to enforce
081  // atomicity.
082
083  /** The name of the attachment which will be used to store the fully resolved target entry. */
084  public static final String AUTHZ_DN_ATTACHMENT;
085  /** The name of the attachment which will be used to store the password attribute. */
086  public static final String PWD_ATTRIBUTE_ATTACHMENT;
087  /** The clear text password, which may not be present if the provided password was pre-encoded. */
088  public static final String CLEAR_PWD_ATTACHMENT;
089  /** A list containing the encoded passwords: plugins can perform changes atomically via CAS. */
090  public static final String ENCODED_PWD_ATTACHMENT;
091
092  static
093  {
094    final String PREFIX = PasswordModifyExtendedOperation.class.getName();
095    AUTHZ_DN_ATTACHMENT = PREFIX + ".AUTHZ_DN";
096    PWD_ATTRIBUTE_ATTACHMENT = PREFIX + ".PWD_ATTRIBUTE";
097    CLEAR_PWD_ATTACHMENT = PREFIX + ".CLEAR_PWD";
098    ENCODED_PWD_ATTACHMENT = PREFIX + ".ENCODED_PWD";
099  }
100  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
101
102  /** The current configuration state. */
103  private PasswordModifyExtendedOperationHandlerCfg currentConfig;
104
105  /** The DN of the identity mapper. */
106  private DN identityMapperDN;
107
108  /** The reference to the identity mapper. */
109  private IdentityMapper<?> identityMapper;
110
111
112  /**
113   * Create an instance of this password modify extended operation.  All initialization should be performed in the
114   * <CODE>initializeExtendedOperationHandler</CODE> method.
115   */
116  public PasswordModifyExtendedOperation()
117  {
118    super(newHashSet(OID_LDAP_NOOP_OPENLDAP_ASSIGNED, OID_PASSWORD_POLICY_CONTROL));
119  }
120
121  @Override
122  public void initializeExtendedOperationHandler(PasswordModifyExtendedOperationHandlerCfg config)
123         throws ConfigException, InitializationException
124  {
125    try
126    {
127      identityMapperDN = config.getIdentityMapperDN();
128      identityMapper = DirectoryServer.getIdentityMapper(identityMapperDN);
129      if (identityMapper == null)
130      {
131        throw new ConfigException(ERR_EXTOP_PASSMOD_NO_SUCH_ID_MAPPER.get(identityMapperDN, config.dn()));
132      }
133    }
134    catch (Exception e)
135    {
136      logger.traceException(e);
137      LocalizableMessage message = ERR_EXTOP_PASSMOD_CANNOT_DETERMINE_ID_MAPPER
138          .get(config.dn(), getExceptionMessage(e));
139      throw new InitializationException(message, e);
140    }
141
142    // Save this configuration for future reference.
143    currentConfig = config;
144
145    // Register this as a change listener.
146    config.addPasswordModifyChangeListener(this);
147
148    super.initializeExtendedOperationHandler(config);
149  }
150
151  @Override
152  public void finalizeExtendedOperationHandler()
153  {
154    currentConfig.removePasswordModifyChangeListener(this);
155
156    super.finalizeExtendedOperationHandler();
157  }
158
159  @Override
160  public void processExtendedOperation(ExtendedOperation operation)
161  {
162    // Initialize the variables associated with components that may be included in the request.
163    ByteString userIdentity = null;
164    ByteString oldPassword  = null;
165    ByteString newPassword  = null;
166
167    // Look at the set of controls included in the request, if there are any.
168    boolean                   noOpRequested        = false;
169    boolean                   pwPolicyRequested    = false;
170    List<Control> controls = operation.getRequestControls();
171    if (controls != null)
172    {
173      for (Control c : controls)
174      {
175        String oid = c.getOID();
176        if (OID_LDAP_NOOP_OPENLDAP_ASSIGNED.equals(oid))
177        {
178          noOpRequested = true;
179        }
180        else if (OID_PASSWORD_POLICY_CONTROL.equals(oid))
181        {
182          pwPolicyRequested = true;
183        }
184      }
185    }
186
187    // Parse the encoded request, if there is one.
188    ByteString requestValue = operation.getRequestValue();
189    if (requestValue != null)
190    {
191      try
192      {
193        ASN1Reader reader = ASN1.getReader(requestValue);
194        reader.readStartSequence();
195        if(reader.hasNextElement() && reader.peekType() == TYPE_PASSWORD_MODIFY_USER_ID)
196        {
197          userIdentity = reader.readOctetString();
198        }
199        if(reader.hasNextElement() && reader.peekType() == TYPE_PASSWORD_MODIFY_OLD_PASSWORD)
200        {
201          oldPassword = reader.readOctetString();
202        }
203        if(reader.hasNextElement() && reader.peekType() == TYPE_PASSWORD_MODIFY_NEW_PASSWORD)
204        {
205          newPassword = reader.readOctetString();
206        }
207        reader.readEndSequence();
208      }
209      catch (Exception ae)
210      {
211        logger.traceException(ae);
212
213        operation.setResultCode(ResultCode.PROTOCOL_ERROR);
214        operation.appendErrorMessage(ERR_EXTOP_PASSMOD_CANNOT_DECODE_REQUEST.get(getExceptionMessage(ae)));
215        return;
216      }
217    }
218
219    // Get the entry for the user that issued the request.
220    Entry requestorEntry = operation.getAuthorizationEntry();
221
222    // See if a user identity was provided.  If so, then try to resolve it to an actual user.
223    DN userDN = null;
224    Entry userEntry = null;
225    DNLock userLock = null;
226    try
227    {
228      if (userIdentity == null)
229      {
230        // This request must be targeted at changing the password for the currently-authenticated user.
231        // Make sure that the user actually is authenticated.
232        ClientConnection   clientConnection = operation.getClientConnection();
233        AuthenticationInfo authInfo = clientConnection.getAuthenticationInfo();
234        if (!authInfo.isAuthenticated() || requestorEntry == null)
235        {
236          operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
237          operation.appendErrorMessage(ERR_EXTOP_PASSMOD_NO_AUTH_OR_USERID.get());
238          return;
239        }
240
241        userDN = requestorEntry.getName();
242        userEntry = requestorEntry;
243      }
244      else
245      {
246        // There was a userIdentity field in the request.
247        String authzIDStr      = userIdentity.toString();
248        String lowerAuthzIDStr = toLowerCase(authzIDStr);
249        if (lowerAuthzIDStr.startsWith("dn:"))
250        {
251          try
252          {
253            userDN = DN.valueOf(authzIDStr.substring(3));
254          }
255          catch (DirectoryException de)
256          {
257            logger.traceException(de);
258
259            operation.setResultCode(ResultCode.INVALID_DN_SYNTAX);
260            operation.appendErrorMessage(ERR_EXTOP_PASSMOD_CANNOT_DECODE_AUTHZ_DN.get(authzIDStr));
261            return;
262          }
263
264          // If the provided DN is an alternate DN for a root user, then replace it with the actual root DN.
265          DN actualRootDN = DirectoryServer.getActualRootBindDN(userDN);
266          if (actualRootDN != null)
267          {
268            userDN = actualRootDN;
269          }
270
271          userEntry = getEntryByDN(operation, userDN);
272          if (userEntry == null)
273          {
274            return;
275          }
276        }
277        else if (lowerAuthzIDStr.startsWith("u:"))
278        {
279          try
280          {
281            userEntry = identityMapper.getEntryForID(authzIDStr.substring(2));
282            if (userEntry == null)
283            {
284              operation.setResultCode(ResultCode.NO_SUCH_OBJECT);
285              operation.appendErrorMessage(ERR_EXTOP_PASSMOD_CANNOT_MAP_USER.get(authzIDStr));
286              return;
287            }
288
289            userDN = userEntry.getName();
290          }
291          catch (DirectoryException de)
292          {
293            logger.traceException(de);
294
295            //Encountered an exception while resolving identity.
296            operation.setResultCode(de.getResultCode());
297            operation.appendErrorMessage(ERR_EXTOP_PASSMOD_ERROR_MAPPING_USER.get(authzIDStr, de.getMessageObject()));
298            return;
299          }
300        }
301        else
302        {
303          /*
304           * the userIdentity provided does not follow Authorization Identity form. RFC3062
305           * declaration "may or may not be an LDAPDN" allows for pretty much anything in that
306           * field. we gonna try to parse it as DN first then if that fails as user ID.
307           */
308          try
309          {
310            userDN = DN.valueOf(authzIDStr);
311          }
312          catch (DirectoryException ignored)
313          {
314            logger.traceException(ignored);
315          }
316
317          if (userDN != null && !userDN.isRootDN()) {
318            // If the provided DN is an alternate DN for a root user, then replace it with the actual root DN.
319            DN actualRootDN = DirectoryServer.getActualRootBindDN(userDN);
320            if (actualRootDN != null) {
321              userDN = actualRootDN;
322            }
323            userEntry = getEntryByDN(operation, userDN);
324          } else {
325            try
326            {
327              userEntry = identityMapper.getEntryForID(authzIDStr);
328            }
329            catch (DirectoryException ignored)
330            {
331              logger.traceException(ignored);
332            }
333          }
334
335          if (userEntry == null) {
336            // The userIdentity was invalid.
337            operation.setResultCode(ResultCode.PROTOCOL_ERROR);
338            operation.appendErrorMessage(ERR_EXTOP_PASSMOD_INVALID_AUTHZID_STRING.get(authzIDStr));
339            return;
340          }
341
342          userDN = userEntry.getName();
343        }
344      }
345
346      userLock = DirectoryServer.getLockManager().tryWriteLockEntry(userDN);
347      if (userLock == null)
348      {
349        operation.setResultCode(ResultCode.BUSY);
350        operation.appendErrorMessage(ERR_EXTOP_PASSMOD_CANNOT_LOCK_USER_ENTRY.get(userDN));
351        return;
352      }
353
354      // At this point, we should have the user entry.  Get the associated password policy.
355      PasswordPolicyState pwPolicyState;
356      try
357      {
358        AuthenticationPolicy policy = AuthenticationPolicy.forUser(userEntry, false);
359        if (!policy.isPasswordPolicy())
360        {
361          operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
362          operation.appendErrorMessage(ERR_EXTOP_PASSMOD_ACCOUNT_NOT_LOCAL.get(userDN));
363          return;
364        }
365        pwPolicyState = (PasswordPolicyState) policy.createAuthenticationPolicyState(userEntry);
366      }
367      catch (DirectoryException de)
368      {
369        logger.traceException(de);
370
371        operation.setResultCode(DirectoryServer.getServerErrorResultCode());
372        operation.appendErrorMessage(ERR_EXTOP_PASSMOD_CANNOT_GET_PW_POLICY.get(userDN, de.getMessageObject()));
373        return;
374      }
375
376      // Determine whether the user is changing his own password or if it's an administrative reset.
377      // If it's an administrative reset, then the requester must have the PASSWORD_RESET privilege.
378      boolean selfChange = isSelfChange(userIdentity, requestorEntry, userDN, oldPassword);
379
380      if (! selfChange)
381      {
382        ClientConnection clientConnection = operation.getClientConnection();
383        if (! clientConnection.hasPrivilege(Privilege.PASSWORD_RESET, operation))
384        {
385          operation.appendErrorMessage(ERR_EXTOP_PASSMOD_INSUFFICIENT_PRIVILEGES.get());
386          operation.setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
387          return;
388        }
389      }
390
391      // See if the account is locked.  If so, then reject the request.
392      if (pwPolicyState.isDisabled())
393      {
394        addPwPolicyErrorResponseControl(operation, pwPolicyRequested, ACCOUNT_LOCKED);
395
396        operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
397        operation.appendErrorMessage(ERR_EXTOP_PASSMOD_ACCOUNT_DISABLED.get());
398        return;
399      }
400      else if (selfChange && pwPolicyState.isLocked())
401      {
402        addPwPolicyErrorResponseControl(operation, pwPolicyRequested, ACCOUNT_LOCKED);
403
404        operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
405        operation.appendErrorMessage(ERR_EXTOP_PASSMOD_ACCOUNT_LOCKED.get());
406        return;
407      }
408
409      // If the current password was provided, then we'll need to verify whether it was correct.
410      // If it wasn't provided but this is a self change, then make sure that's OK.
411      if (oldPassword == null)
412      {
413        if (selfChange
414            && pwPolicyState.getAuthenticationPolicy().isPasswordChangeRequiresCurrentPassword())
415        {
416          addPwPolicyErrorResponseControl(operation, pwPolicyRequested, MUST_SUPPLY_OLD_PASSWORD);
417
418          operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
419          operation.appendErrorMessage(ERR_EXTOP_PASSMOD_REQUIRE_CURRENT_PW.get());
420          return;
421        }
422      }
423      else
424      {
425        if (pwPolicyState.getAuthenticationPolicy().isRequireSecureAuthentication()
426            && !operation.getClientConnection().isSecure())
427        {
428          operation.setResultCode(ResultCode.CONFIDENTIALITY_REQUIRED);
429          operation.addAdditionalLogItem(AdditionalLogItem.quotedKeyValue(getClass(), "additionalInfo",
430              ERR_EXTOP_PASSMOD_SECURE_AUTH_REQUIRED.get()));
431          return;
432        }
433
434        if (pwPolicyState.passwordMatches(oldPassword))
435        {
436          pwPolicyState.setLastLoginTime();
437        }
438        else
439        {
440          operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
441          operation.addAdditionalLogItem(AdditionalLogItem.quotedKeyValue(getClass(), "additionalInfo",
442              ERR_EXTOP_PASSMOD_INVALID_OLD_PASSWORD.get()));
443
444          pwPolicyState.updateAuthFailureTimes();
445          List<Modification> mods = pwPolicyState.getModifications();
446          if (! mods.isEmpty())
447          {
448            getRootConnection().processModify(userDN, mods);
449          }
450
451          return;
452        }
453      }
454
455      // If it is a self password change and we don't allow that, then reject the request.
456      if (selfChange
457          && !pwPolicyState.getAuthenticationPolicy().isAllowUserPasswordChanges())
458      {
459        addPwPolicyErrorResponseControl(operation, pwPolicyRequested, PASSWORD_MOD_NOT_ALLOWED);
460
461        operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
462        operation.appendErrorMessage(ERR_EXTOP_PASSMOD_USER_PW_CHANGES_NOT_ALLOWED.get());
463        return;
464      }
465
466      // If we require secure password changes and the connection isn't secure, then reject the request.
467      if (pwPolicyState.getAuthenticationPolicy().isRequireSecurePasswordChanges()
468          && !operation.getClientConnection().isSecure())
469      {
470        operation.setResultCode(ResultCode.CONFIDENTIALITY_REQUIRED);
471        operation.appendErrorMessage(ERR_EXTOP_PASSMOD_SECURE_CHANGES_REQUIRED.get());
472        return;
473      }
474
475      // If it's a self-change request and the user is within the minimum age, then reject it.
476      if (selfChange && pwPolicyState.isWithinMinimumAge())
477      {
478        addPwPolicyErrorResponseControl(operation, pwPolicyRequested, PASSWORD_TOO_YOUNG);
479
480        operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
481        operation.appendErrorMessage(ERR_EXTOP_PASSMOD_IN_MIN_AGE.get());
482        return;
483      }
484
485      // If the user's password is expired and it's a self-change request, then see if that's OK.
486      if (selfChange
487          && pwPolicyState.isPasswordExpired()
488          && !pwPolicyState.getAuthenticationPolicy().isAllowExpiredPasswordChanges())
489      {
490        addPwPolicyErrorResponseControl(operation, pwPolicyRequested, PasswordPolicyErrorType.PASSWORD_EXPIRED);
491
492        operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
493        operation.appendErrorMessage(ERR_EXTOP_PASSMOD_PASSWORD_IS_EXPIRED.get());
494        return;
495      }
496
497      // If the a new password was provided, then perform any appropriate validation on it.
498      // If not, then see if we can generate one.
499      boolean generatedPassword = false;
500      boolean isPreEncoded      = false;
501      if (newPassword == null)
502      {
503        try
504        {
505          newPassword = pwPolicyState.generatePassword();
506          if (newPassword == null)
507          {
508            operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
509            operation.appendErrorMessage(ERR_EXTOP_PASSMOD_NO_PW_GENERATOR.get());
510            return;
511          }
512
513          generatedPassword = true;
514        }
515        catch (DirectoryException de)
516        {
517          logger.traceException(de);
518          operation.setResultCode(de.getResultCode());
519          operation.appendErrorMessage(ERR_EXTOP_PASSMOD_CANNOT_GENERATE_PW.get(de.getMessageObject()));
520          return;
521        }
522        // Prepare to update the password history, if necessary.
523        if (pwPolicyState.maintainHistory())
524        {
525          if (pwPolicyState.isPasswordInHistory(newPassword))
526          {
527            operation.setResultCode(ResultCode.CONSTRAINT_VIOLATION);
528            operation.appendErrorMessage(ERR_EXTOP_PASSMOD_PW_IN_HISTORY.get());
529            return;
530          }
531          else
532          {
533            pwPolicyState.updatePasswordHistory();
534          }
535        }
536      }
537      else if (pwPolicyState.passwordIsPreEncoded(newPassword))
538      {
539        // The password modify extended operation isn't intended to be invoked
540        // by an internal operation or during synchronization, so we don't
541        // need to check for those cases.
542        isPreEncoded = true;
543        if (!pwPolicyState.getAuthenticationPolicy().isAllowPreEncodedPasswords())
544        {
545          operation.setResultCode(ResultCode.CONSTRAINT_VIOLATION);
546          operation.appendErrorMessage(ERR_EXTOP_PASSMOD_PRE_ENCODED_NOT_ALLOWED.get());
547          return;
548        }
549      }
550      else
551      {
552        // Run the new password through the set of password validators.
553        if (selfChange || !pwPolicyState.getAuthenticationPolicy().isSkipValidationForAdministrators())
554        {
555          Set<ByteString> clearPasswords = new HashSet<>(pwPolicyState.getClearPasswords());
556          if (oldPassword != null)
557          {
558            clearPasswords.add(oldPassword);
559          }
560
561          LocalizableMessageBuilder invalidReason = new LocalizableMessageBuilder();
562          if (!pwPolicyState.passwordIsAcceptable(operation, userEntry, newPassword, clearPasswords, invalidReason))
563          {
564            addPwPolicyErrorResponseControl(operation, pwPolicyRequested, INSUFFICIENT_PASSWORD_QUALITY);
565
566            operation.setResultCode(ResultCode.CONSTRAINT_VIOLATION);
567            operation.appendErrorMessage(ERR_EXTOP_PASSMOD_UNACCEPTABLE_PW.get(invalidReason));
568            return;
569          }
570        }
571
572        // Prepare to update the password history, if necessary.
573        if (pwPolicyState.maintainHistory())
574        {
575          if (pwPolicyState.isPasswordInHistory(newPassword))
576          {
577            if (selfChange || !pwPolicyState.getAuthenticationPolicy().isSkipValidationForAdministrators())
578            {
579              operation.setResultCode(ResultCode.CONSTRAINT_VIOLATION);
580              operation.appendErrorMessage(ERR_EXTOP_PASSMOD_PW_IN_HISTORY.get());
581              return;
582            }
583          }
584          else
585          {
586            pwPolicyState.updatePasswordHistory();
587          }
588        }
589      }
590
591      // Get the encoded forms of the new password.
592      List<ByteString> encodedPasswords;
593      if (isPreEncoded)
594      {
595        encodedPasswords = newArrayList(newPassword);
596      }
597      else
598      {
599        try
600        {
601          encodedPasswords = pwPolicyState.encodePassword(newPassword);
602        }
603        catch (DirectoryException de)
604        {
605          logger.traceException(de);
606
607          operation.setResultCode(de.getResultCode());
608          operation.appendErrorMessage(ERR_EXTOP_PASSMOD_CANNOT_ENCODE_PASSWORD.get(de.getMessageObject()));
609          return;
610        }
611      }
612
613      // If the current password was provided, then remove all matching values from the user's entry
614      // and replace them with the new password.  Otherwise replace all password values.
615      AttributeType attrType = pwPolicyState.getAuthenticationPolicy().getPasswordAttribute();
616      List<Modification> modList = new ArrayList<>();
617      if (oldPassword != null)
618      {
619        // Remove all existing encoded values that match the old password.
620        Set<ByteString> existingValues = pwPolicyState.getPasswordValues();
621        Set<ByteString> deleteValues = new LinkedHashSet<>(existingValues.size());
622
623        for (ByteString v : existingValues)
624        {
625          try
626          {
627            String[] components = decodePassword(pwPolicyState, v.toString());
628            PasswordStorageScheme<?> scheme = getPasswordStorageScheme(pwPolicyState, components[0]);
629            if (// The password is encoded using an unknown scheme.  Remove it from the user's entry.
630                scheme == null
631                || passwordMatches(pwPolicyState, scheme, oldPassword, components))
632            {
633              deleteValues.add(v);
634            }
635          }
636          catch (DirectoryException de)
637          {
638            logger.traceException(de);
639
640            // We couldn't decode the provided password value, so remove it from the user's entry.
641            deleteValues.add(v);
642          }
643        }
644
645        modList.add(newModification(ModificationType.DELETE, attrType, deleteValues));
646        modList.add(newModification(ModificationType.ADD, attrType, encodedPasswords));
647      }
648      else
649      {
650        modList.add(newModification(ModificationType.REPLACE, attrType, encodedPasswords));
651      }
652
653      // Update the password changed time for the user entry.
654      pwPolicyState.setPasswordChangedTime();
655
656      // If the password was changed by an end user, then clear any reset flag that might exist.
657      // If the password was changed by an administrator, then see if we need to set the reset flag.
658      pwPolicyState.setMustChangePassword(
659          !selfChange && pwPolicyState.getAuthenticationPolicy().isForceChangeOnReset());
660
661      // Clear any record of grace logins, auth failures, and expiration warnings.
662      pwPolicyState.clearFailureLockout();
663      pwPolicyState.clearGraceLoginTimes();
664      pwPolicyState.clearWarnedTime();
665
666      // If the LDAP no-op control was included in the request, then set the
667      // appropriate response.  Otherwise, process the operation.
668      if (noOpRequested)
669      {
670        operation.appendErrorMessage(WARN_EXTOP_PASSMOD_NOOP.get());
671        operation.setResultCode(ResultCode.NO_OPERATION);
672        return;
673      }
674
675      if (selfChange && requestorEntry == null)
676      {
677        requestorEntry = userEntry;
678      }
679
680      // Get an internal connection and use it to perform the modification.
681      boolean isRoot = DirectoryServer.isRootDN(requestorEntry.getName());
682      AuthenticationInfo authInfo = new AuthenticationInfo(requestorEntry, isRoot);
683      InternalClientConnection internalConnection = new InternalClientConnection(authInfo);
684
685      ModifyOperation modifyOperation = internalConnection.processModify(userDN, modList);
686      ResultCode resultCode = modifyOperation.getResultCode();
687      if (resultCode != ResultCode.SUCCESS)
688      {
689        operation.setResultCode(resultCode);
690        operation.setErrorMessage(modifyOperation.getErrorMessage());
691        // FIXME should it also call setMatchedDN()
692        operation.setReferralURLs(modifyOperation.getReferralURLs());
693        return;
694      }
695
696      // If there were any password policy state changes, we need to apply
697      // them using a root connection because the end user may not have
698      // sufficient access to apply them.  This is less efficient than
699      // doing them all in the same modification, but it's safer.
700      List<Modification> pwPolicyMods = pwPolicyState.getModifications();
701      if (! pwPolicyMods.isEmpty())
702      {
703        ModifyOperation modOp = getRootConnection().processModify(userDN, pwPolicyMods);
704        if (modOp.getResultCode() != ResultCode.SUCCESS)
705        {
706          // At this point, the user's password is already changed so there's
707          // not much point in returning a non-success result.  However, we
708          // should at least log that something went wrong.
709          logger.warn(WARN_EXTOP_PASSMOD_CANNOT_UPDATE_PWP_STATE, userDN, modOp.getResultCode(),
710              modOp.getErrorMessage());
711        }
712      }
713
714      // If we've gotten here, then everything is OK, so indicate that the operation was successful.
715      operation.setResultCode(ResultCode.SUCCESS);
716
717      // Save attachments for post-op plugins (e.g. Samba password plugin).
718      operation.setAttachment(AUTHZ_DN_ATTACHMENT, userDN);
719      operation.setAttachment(PWD_ATTRIBUTE_ATTACHMENT, pwPolicyState.getAuthenticationPolicy().getPasswordAttribute());
720      if (!isPreEncoded)
721      {
722        operation.setAttachment(CLEAR_PWD_ATTACHMENT, newPassword);
723      }
724      operation.setAttachment(ENCODED_PWD_ATTACHMENT, encodedPasswords);
725
726      // If a password was generated, then include it in the response.
727      if (generatedPassword)
728      {
729        ByteStringBuilder builder = new ByteStringBuilder();
730        ASN1Writer writer = ASN1.getWriter(builder);
731
732        try
733        {
734          writer.writeStartSequence();
735          writer.writeOctetString(TYPE_PASSWORD_MODIFY_GENERATED_PASSWORD, newPassword);
736          writer.writeEndSequence();
737        }
738        catch (IOException e)
739        {
740          logger.traceException(e);
741        }
742
743        operation.setResponseValue(builder.toByteString());
744      }
745
746
747      // If this was a self password change, and the client is authenticated as the user whose password was changed,
748      // then clear the "must change password" flag in the client connection.  Note that we're using the
749      // authentication DN rather than the authorization DN in this case to avoid mistakenly clearing the flag
750      // for the wrong user.
751      if (selfChange
752          && authInfo.getAuthenticationDN() != null
753          && authInfo.getAuthenticationDN().equals(userDN))
754      {
755        operation.getClientConnection().setMustChangePassword(false);
756      }
757
758      addPwPolicyErrorResponseControl(operation, pwPolicyRequested, null);
759
760      generateAccountStatusNotification(oldPassword, newPassword, userEntry, pwPolicyState, selfChange);
761    }
762    finally
763    {
764      if (userLock != null)
765      {
766        userLock.unlock();
767      }
768    }
769  }
770
771  private void addPwPolicyErrorResponseControl(ExtendedOperation operation, boolean pwPolicyRequested,
772      PasswordPolicyErrorType pwPolicyErrorType)
773  {
774    if (pwPolicyRequested)
775    {
776      operation.addResponseControl(new PasswordPolicyResponseControl(null, 0, pwPolicyErrorType));
777    }
778  }
779
780  private void generateAccountStatusNotification(ByteString oldPassword, ByteString newPassword, Entry userEntry,
781      PasswordPolicyState pwPolicyState, boolean selfChange)
782  {
783    List<ByteString> currentPasswords = null;
784    if (oldPassword != null)
785    {
786      currentPasswords = newArrayList(oldPassword);
787    }
788    List<ByteString> newPasswords = newArrayList(newPassword);
789
790    Map<AccountStatusNotificationProperty, List<String>> notifProperties =
791        AccountStatusNotification.createProperties(pwPolicyState, false, -1, currentPasswords, newPasswords);
792    if (selfChange)
793    {
794      pwPolicyState.generateAccountStatusNotification(
795          PASSWORD_CHANGED, userEntry, INFO_MODIFY_PASSWORD_CHANGED.get(), notifProperties);
796    }
797    else
798    {
799      pwPolicyState.generateAccountStatusNotification(
800          PASSWORD_RESET, userEntry, INFO_MODIFY_PASSWORD_RESET.get(), notifProperties);
801    }
802  }
803
804  private String[] decodePassword(PasswordPolicyState pwPolicyState, String encodedPassword) throws DirectoryException
805  {
806    return pwPolicyState.getAuthenticationPolicy().isAuthPasswordSyntax()
807        ? AuthPasswordSyntax.decodeAuthPassword(encodedPassword)
808        : UserPasswordSyntax.decodeUserPassword(encodedPassword);
809  }
810
811  private PasswordStorageScheme<?> getPasswordStorageScheme(PasswordPolicyState pwPolicyState, String scheme)
812  {
813    return pwPolicyState.getAuthenticationPolicy().isAuthPasswordSyntax()
814        ? DirectoryServer.getAuthPasswordStorageScheme(scheme)
815        : DirectoryServer.getPasswordStorageScheme(toLowerCase(scheme));
816  }
817
818  private boolean passwordMatches(
819      PasswordPolicyState pwPolicyState, PasswordStorageScheme<?> scheme, ByteString oldPassword, String[] components)
820  {
821    return pwPolicyState.getAuthenticationPolicy().isAuthPasswordSyntax()
822        ? scheme.authPasswordMatches(oldPassword, components[1], components[2])
823        : scheme.passwordMatches(oldPassword, ByteString.valueOfUtf8(components[1]));
824  }
825
826  private boolean isSelfChange(ByteString userIdentity, Entry requestorEntry, DN userDN, ByteString oldPassword)
827  {
828    if (userIdentity == null)
829    {
830      return true;
831    }
832    else if (requestorEntry != null)
833    {
834      return userDN.equals(requestorEntry.getName());
835    }
836    else
837    {
838      return oldPassword != null;
839    }
840  }
841
842  private Modification newModification(ModificationType modType, AttributeType attrType, Collection<ByteString> value)
843  {
844    AttributeBuilder builder = new AttributeBuilder(attrType);
845    builder.addAll(value);
846    return new Modification(modType, builder.toAttribute());
847  }
848
849
850  /**
851   * Retrieves the entry for the specified user based on the provided DN.  If any problem is encountered or
852   * the requested entry does not exist, then the provided operation will be updated with appropriate result
853   * information and this method will return <CODE>null</CODE>.
854   * The caller must hold a write lock on the specified entry.
855   *
856   * @param  operation  The extended operation being processed.
857   * @param  entryDN    The DN of the user entry to retrieve.
858   *
859   * @return  The requested entry, or <CODE>null</CODE> if there was no such entry or it could not be retrieved.
860   */
861  private Entry getEntryByDN(ExtendedOperation operation, DN entryDN)
862  {
863    // Retrieve the user's entry from the directory.  If it does not exist, then fail.
864    try
865    {
866      Entry userEntry = DirectoryServer.getEntry(entryDN);
867
868      if (userEntry == null)
869      {
870        operation.setResultCode(ResultCode.NO_SUCH_OBJECT);
871        operation.appendErrorMessage(ERR_EXTOP_PASSMOD_NO_USER_ENTRY_BY_AUTHZID.get(entryDN));
872
873        // See if one of the entry's ancestors exists.
874        operation.setMatchedDN(findMatchedDN(entryDN));
875        return null;
876      }
877
878      return userEntry;
879    }
880    catch (DirectoryException de)
881    {
882      logger.traceException(de);
883
884      operation.setResultCode(de.getResultCode());
885      operation.appendErrorMessage(de.getMessageObject());
886      operation.setMatchedDN(de.getMatchedDN());
887      operation.setReferralURLs(de.getReferralURLs());
888      return null;
889    }
890  }
891
892  private DN findMatchedDN(DN entryDN)
893  {
894    try
895    {
896      DN matchedDN = entryDN.getParentDNInSuffix();
897      while (matchedDN != null)
898      {
899        if (DirectoryServer.entryExists(matchedDN))
900        {
901          return matchedDN;
902        }
903
904        matchedDN = matchedDN.getParentDNInSuffix();
905      }
906    }
907    catch (Exception e)
908    {
909      logger.traceException(e);
910    }
911    return null;
912  }
913
914  @Override
915  public boolean isConfigurationAcceptable(ExtendedOperationHandlerCfg configuration,
916                                           List<LocalizableMessage> unacceptableReasons)
917  {
918    PasswordModifyExtendedOperationHandlerCfg config = (PasswordModifyExtendedOperationHandlerCfg) configuration;
919    return isConfigurationChangeAcceptable(config, unacceptableReasons);
920  }
921
922  @Override
923  public boolean isConfigurationChangeAcceptable(PasswordModifyExtendedOperationHandlerCfg config,
924                                                 List<LocalizableMessage> unacceptableReasons)
925  {
926    try
927    {
928      // Make sure that the specified identity mapper is OK.
929      DN mapperDN = config.getIdentityMapperDN();
930      IdentityMapper<?> mapper = DirectoryServer.getIdentityMapper(mapperDN);
931      if (mapper == null)
932      {
933        unacceptableReasons.add(ERR_EXTOP_PASSMOD_NO_SUCH_ID_MAPPER.get(mapperDN, config.dn()));
934        return false;
935      }
936      return true;
937    }
938    catch (Exception e)
939    {
940      logger.traceException(e);
941
942      unacceptableReasons.add(ERR_EXTOP_PASSMOD_CANNOT_DETERMINE_ID_MAPPER.get(config.dn(), getExceptionMessage(e)));
943      return false;
944    }
945  }
946
947  @Override
948  public ConfigChangeResult applyConfigurationChange(PasswordModifyExtendedOperationHandlerCfg config)
949  {
950    final ConfigChangeResult ccr = new ConfigChangeResult();
951
952    // Make sure that the specified identity mapper is OK.
953    DN             mapperDN = null;
954    IdentityMapper<?> mapper   = null;
955    try
956    {
957      mapperDN = config.getIdentityMapperDN();
958      mapper   = DirectoryServer.getIdentityMapper(mapperDN);
959      if (mapper == null)
960      {
961        ccr.setResultCode(ResultCode.CONSTRAINT_VIOLATION);
962        ccr.addMessage(ERR_EXTOP_PASSMOD_NO_SUCH_ID_MAPPER.get(mapperDN, config.dn()));
963      }
964    }
965    catch (Exception e)
966    {
967      logger.traceException(e);
968
969      ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
970      ccr.addMessage(ERR_EXTOP_PASSMOD_CANNOT_DETERMINE_ID_MAPPER.get(config.dn(), getExceptionMessage(e)));
971    }
972
973    // If all of the changes were acceptable, then apply them.
974    if (ccr.getResultCode() == ResultCode.SUCCESS
975        && ! identityMapperDN.equals(mapperDN))
976    {
977      identityMapper   = mapper;
978      identityMapperDN = mapperDN;
979    }
980
981    // Save this configuration for future reference.
982    currentConfig = config;
983
984    return ccr;
985  }
986
987  @Override
988  public String getExtendedOperationOID()
989  {
990    return OID_PASSWORD_MODIFY_REQUEST;
991  }
992
993  @Override
994  public String getExtendedOperationName()
995  {
996    return "Password Modify";
997  }
998}