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