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