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.core;
018
019import static org.opends.messages.CoreMessages.*;
020import static org.opends.server.config.ConfigConstants.*;
021import static org.opends.server.protocols.internal.InternalClientConnection.*;
022import static org.opends.server.schema.SchemaConstants.*;
023import static org.opends.server.util.StaticUtils.*;
024
025import java.text.ParseException;
026import java.text.SimpleDateFormat;
027import java.util.ArrayList;
028import java.util.Collection;
029import java.util.Collections;
030import java.util.Date;
031import java.util.HashSet;
032import java.util.Iterator;
033import java.util.LinkedHashSet;
034import java.util.LinkedList;
035import java.util.List;
036import java.util.Map;
037import java.util.Set;
038import java.util.TimeZone;
039import java.util.TreeMap;
040
041import org.forgerock.i18n.LocalizableMessage;
042import org.forgerock.i18n.LocalizableMessageBuilder;
043import org.forgerock.i18n.slf4j.LocalizedLogger;
044import org.forgerock.opendj.ldap.ByteString;
045import org.forgerock.opendj.ldap.ConditionResult;
046import org.forgerock.opendj.ldap.GeneralizedTime;
047import org.forgerock.opendj.ldap.ModificationType;
048import org.forgerock.opendj.ldap.ResultCode;
049import org.forgerock.opendj.ldap.schema.AttributeType;
050import org.forgerock.opendj.server.config.meta.PasswordPolicyCfgDefn;
051import org.opends.server.api.AccountStatusNotificationHandler;
052import org.opends.server.api.AuthenticationPolicyState;
053import org.opends.server.api.PasswordGenerator;
054import org.opends.server.api.PasswordStorageScheme;
055import org.opends.server.api.PasswordValidator;
056import org.opends.server.protocols.internal.InternalClientConnection;
057import org.opends.server.protocols.ldap.LDAPAttribute;
058import org.opends.server.schema.AuthPasswordSyntax;
059import org.opends.server.schema.GeneralizedTimeSyntax;
060import org.opends.server.schema.UserPasswordSyntax;
061import org.opends.server.types.AccountStatusNotification;
062import org.opends.server.types.AccountStatusNotificationProperty;
063import org.opends.server.types.AccountStatusNotificationType;
064import org.opends.server.types.Attribute;
065import org.opends.server.types.AttributeBuilder;
066import org.opends.server.types.Attributes;
067import org.opends.server.types.DirectoryException;
068import org.opends.server.types.Entry;
069import org.opends.server.types.Modification;
070import org.opends.server.types.Operation;
071import org.opends.server.types.RawModification;
072
073/**
074 * This class provides a data structure for holding password policy state
075 * information for a user account.
076 */
077public final class PasswordPolicyState extends AuthenticationPolicyState
078{
079  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
080
081  /** The string representation of the user's DN. */
082  private final String userDNString;
083
084  /** The password policy with which the account is associated. */
085  private final PasswordPolicy passwordPolicy;
086
087  /** The current time for use in all password policy calculations. */
088  private final long currentTime;
089
090  /** The time that the user's password was last changed. */
091  private long passwordChangedTime = Long.MIN_VALUE;
092
093  /** Indicates whether the user's account is expired. */
094  private ConditionResult isAccountExpired = ConditionResult.UNDEFINED;
095  /** Indicates whether the user's password is expired. */
096  private ConditionResult isPasswordExpired = ConditionResult.UNDEFINED;
097  /** Indicates whether the warning to send to the client would be the first warning for the user. */
098  private ConditionResult isFirstWarning = ConditionResult.UNDEFINED;
099  /** Indicates whether the user's account is locked by the idle lockout. */
100  private ConditionResult isIdleLocked = ConditionResult.UNDEFINED;
101  /**
102   * Indicates whether the user may use a grace login if the password is expired and there are one
103   * or more grace logins remaining.
104   */
105  private ConditionResult mayUseGraceLogin = ConditionResult.UNDEFINED;
106  /** Indicates whether the user's password must be changed. */
107  private ConditionResult mustChangePassword = ConditionResult.UNDEFINED;
108  /** Indicates whether the user should be warned of an upcoming expiration. */
109  private ConditionResult shouldWarn = ConditionResult.UNDEFINED;
110
111  /** The number of seconds until the user's account is automatically unlocked. */
112  private int secondsUntilUnlock = Integer.MIN_VALUE;
113
114  /** The set of authentication failure times for this user. */
115  private List<Long> authFailureTimes;
116  /** The set of grace login times for this user. */
117  private List<Long> graceLoginTimes;
118
119  /** The time that the user's account should expire (or did expire). */
120  private long accountExpirationTime = Long.MIN_VALUE;
121  /** The time that the user's entry was locked due to too many authentication failures. */
122  private long failureLockedTime = Long.MIN_VALUE;
123  /** The time that the user last authenticated to the Directory Server. */
124  private long lastLoginTime = Long.MIN_VALUE;
125  /** The time that the user's password should expire (or did expire). */
126  private long passwordExpirationTime = Long.MIN_VALUE;
127  /** The last required change time with which the user complied. */
128  private long requiredChangeTime = Long.MIN_VALUE;
129  /** The time that the user was first warned about an upcoming expiration. */
130  private long warnedTime = Long.MIN_VALUE;
131
132  /** The set of modifications that should be applied to the user's entry. */
133  private final LinkedList<Modification> modifications = new LinkedList<>();
134
135  /**
136   * Creates a new password policy state object with the provided information.
137   * <p>
138   * Note that this version of the constructor should only be used for testing purposes when the tests should be
139   * evaluated with a fixed time rather than the actual current time. For all other purposes, the other constructor
140   * should be used.
141   * </p>
142   *
143   * @param policy      The password policy associated with the state.
144   * @param userEntry   The entry with the user account.
145   * @param currentTime The time to use as the current time for all time-related determinations.
146   */
147  PasswordPolicyState(PasswordPolicy policy, Entry userEntry, long currentTime)
148  {
149    super(userEntry);
150    this.currentTime = currentTime;
151    this.userDNString = userEntry.getName().toString();
152    this.passwordPolicy = policy;
153  }
154
155   /**
156    * Retrieves the value of the specified attribute as a string.
157    *
158    * @param  attributeType  The attribute type whose value should be retrieved.
159    *
160    * @return  The value of the specified attribute as a string, or <CODE>null</CODE> if there is no such value.
161    */
162  private String getValue(AttributeType attributeType)
163  {
164    Attribute attr = getFirstAttributeNotEmpty(attributeType);
165    String stringValue = attr != null ? attr.iterator().next().toString() : null;
166    if (logger.isTraceEnabled())
167    {
168      if (stringValue != null)
169      {
170        logger.trace("Returning value %s for user %s", stringValue, userDNString);
171      }
172      else
173      {
174        logger.trace("Returning null because attribute %s does not exist in user entry %s",
175            attributeType.getNameOrOID(), userDNString);
176      }
177    }
178
179    return stringValue;
180  }
181
182  private Attribute getFirstAttributeNotEmpty(AttributeType attributeType)
183  {
184    for (Attribute a : userEntry.getAttribute(attributeType))
185    {
186      if (!a.isEmpty())
187      {
188        return a;
189      }
190    }
191    return null;
192  }
193
194  /**
195   * Retrieves the set of values of the specified attribute from the user's entry in generalized time format.
196   *
197   * @param  attributeType  The attribute type whose values should be parsed as generalized time values.
198   *
199   * @return  The set of generalized time values, or an empty list if there are none.
200   *
201   * @throws  DirectoryException  If a problem occurs while attempting to decode a value as a generalized time.
202   */
203  private List<Long> getGeneralizedTimes(AttributeType attributeType)
204          throws DirectoryException
205  {
206    ArrayList<Long> timeValues = new ArrayList<>();
207
208    for (Attribute a : userEntry.getAttribute(attributeType))
209    {
210      for (ByteString v : a)
211      {
212        try
213        {
214          timeValues.add(GeneralizedTime.valueOf(v.toString()).getTimeInMillis());
215        }
216        catch (Exception e)
217        {
218          logger.traceException(e, "Unable to decode value %s for attribute %s in user entry %s",
219              v, attributeType.getNameOrOID(), userDNString);
220
221          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
222              ERR_PWPSTATE_CANNOT_DECODE_GENERALIZED_TIME.get(v, attributeType.getNameOrOID(), userDNString, e),
223              e);
224        }
225      }
226    }
227
228    if (timeValues.isEmpty())
229    {
230      logger.trace("Returning an empty list because attribute %s does not exist in user entry %s",
231          attributeType.getNameOrOID(), userDNString);
232    }
233    return timeValues;
234  }
235
236  /**
237   * Get the password storage scheme used by a given password value.
238   *
239   * @param  v  The encoded password value to check.
240   *
241   * @return  The scheme used by the password.
242   *
243   * @throws  DirectoryException  If the password could not be decoded.
244   */
245  private PasswordStorageScheme<?> getPasswordStorageScheme(ByteString v) throws DirectoryException
246  {
247    if (passwordPolicy.isAuthPasswordSyntax())
248    {
249      String[] pwComps = AuthPasswordSyntax.decodeAuthPassword(v.toString());
250      return DirectoryServer.getAuthPasswordStorageScheme(pwComps[0]);
251    }
252    else
253    {
254      String[] pwComps = UserPasswordSyntax.decodeUserPassword(v.toString());
255      return DirectoryServer.getPasswordStorageScheme(pwComps[0]);
256    }
257  }
258
259  @Override
260  public PasswordPolicy getAuthenticationPolicy()
261  {
262    return passwordPolicy;
263  }
264
265  /**
266   * Retrieves the time that the password was last changed.
267   *
268   * @return  The time that the password was last changed.
269   */
270  public long getPasswordChangedTime()
271  {
272    if (passwordChangedTime < 0)
273    {
274      // Get the password changed time for the user.
275      try
276      {
277        passwordChangedTime = getGeneralizedTime0(userEntry, OP_ATTR_PWPOLICY_CHANGED_TIME_LC);
278      }
279      catch (DirectoryException e)
280      {
281        /*
282         * The password change time could not be parsed (but has been logged in the debug log).
283         * The best effort we can do from here is to a) use the current time, b) use the start
284         * of the epoch (1/1/1970), or c) use the create time stamp. Lets treat this problem as if the change time
285         * attribute did not exist and resort to the create time stamp.
286         */
287      }
288
289      if (passwordChangedTime < 0)
290      {
291        // Get the time that the user's account was created.
292        try
293        {
294          passwordChangedTime = getGeneralizedTime0(userEntry, OP_ATTR_CREATE_TIMESTAMP_LC);
295        }
296        catch (DirectoryException e)
297        {
298          /*
299           * The create time stamp could not be parsed (but has been logged in the debug log).
300           * The best effort we can do from here is to a) use the current time, or b) use the start of
301            * the epoch (1/1/1970). Lets treat this problem as if the change time attribute did not exist
302           * and use the start of the epoch. Doing so stands a greater chance of forcing a password change.
303           */
304        }
305
306        if (passwordChangedTime < 0)
307        {
308          passwordChangedTime = 0;
309
310          if (logger.isTraceEnabled())
311          {
312            logger.trace(
313                "Could not determine password changed time for " + "user %s.", userDNString);
314          }
315        }
316      }
317    }
318
319    return passwordChangedTime;
320  }
321
322  private long getGeneralizedTime0(Entry userEntry, String attrName) throws DirectoryException
323  {
324    return getGeneralizedTime(userEntry, DirectoryServer.getSchema().getAttributeType(attrName));
325  }
326
327  /**
328   * Retrieves the time that this password policy state object was created.
329   *
330   * @return  The time that this password policy state object was created.
331   */
332  public long getCurrentTime()
333  {
334    return currentTime;
335  }
336
337  /**
338   * Retrieves the unmodifiable set of values for the password attribute from the user entry.
339   *
340   * @return The unmodifiable set of values for the password attribute from the user entry.
341   */
342  public Set<ByteString> getPasswordValues()
343  {
344    final Attribute attr = getFirstAttributeNotEmpty(passwordPolicy.getPasswordAttribute());
345    if (attr != null)
346    {
347      Set<ByteString> values = new LinkedHashSet<>(attr.size());
348      for (ByteString value : attr)
349      {
350        values.add(value);
351      }
352      return Collections.unmodifiableSet(values);
353    }
354    return Collections.emptySet();
355  }
356
357  /** Sets a new value for the password changed time equal to the current time. */
358  public void setPasswordChangedTime()
359  {
360    setPasswordChangedTime(currentTime);
361  }
362
363  /**
364   * Sets a new value for the password changed time equal to the specified time.
365   * This method should generally only be used for testing purposes, since the variant that uses
366   * the current time is preferred almost everywhere else.
367   *
368   * @param  passwordChangedTime  The time to use
369   */
370  public void setPasswordChangedTime(long passwordChangedTime)
371  {
372    if (logger.isTraceEnabled())
373    {
374      logger.trace("Setting password changed time for user %s to current time of %d", userDNString, currentTime);
375    }
376
377    // passwordChangedTime is computed in the constructor from values in the entry.
378    if (getPasswordChangedTime() != passwordChangedTime)
379    {
380      this.passwordChangedTime = passwordChangedTime;
381      replaceAttribute(OP_ATTR_PWPOLICY_CHANGED_TIME, GeneralizedTimeSyntax.format(passwordChangedTime));
382    }
383  }
384
385  /**
386   * Removes the password changed time value from the user's entry.  This should only be used for testing
387   * purposes, as it can really mess things up if you don't know what you're doing.
388   */
389  public void clearPasswordChangedTime()
390  {
391    logger.trace("Clearing password changed time for user %s", userDNString);
392
393    clearAttribute(OP_ATTR_PWPOLICY_CHANGED_TIME_LC);
394
395    // Fall back to using the entry creation time as the password changed time, if it's defined.
396    // Otherwise, use a value of zero.
397    try
398    {
399      passwordChangedTime = getGeneralizedTime0(userEntry, OP_ATTR_CREATE_TIMESTAMP_LC);
400      if (passwordChangedTime < 0)
401      {
402        passwordChangedTime = 0;
403      }
404    }
405    catch (Exception e)
406    {
407      passwordChangedTime = 0;
408    }
409  }
410
411  /**
412   * Updates the user entry to indicate whether user account has been administratively disabled.
413   *
414   * @param isDisabled
415   *          Indicates whether the user account has been administratively disabled.
416   */
417  public void setDisabled(boolean isDisabled)
418  {
419    if (logger.isTraceEnabled())
420    {
421      logger.trace("Updating user %s to set the disabled flag to %b", userDNString, isDisabled);
422    }
423
424    if (isDisabled == isDisabled())
425    {
426      return; // requested state matches current state
427    }
428
429    this.isDisabled = ConditionResult.not(this.isDisabled);
430    replaceAttribute(OP_ATTR_ACCOUNT_DISABLED, isDisabled);
431  }
432
433  /**
434   * Indicates whether the user's account is currently expired.
435   *
436   * @return  <CODE>true</CODE> if the user's account is expired, or <CODE>false</CODE> if not.
437   */
438  public boolean isAccountExpired()
439  {
440    if (isAccountExpired != ConditionResult.UNDEFINED)
441    {
442      if (logger.isTraceEnabled())
443      {
444        logger.trace("Returning stored result of %b for user %s",
445            isAccountExpired == ConditionResult.TRUE, userDNString);
446      }
447
448      return isAccountExpired == ConditionResult.TRUE;
449    }
450
451    try {
452      accountExpirationTime = getGeneralizedTime0(userEntry, OP_ATTR_ACCOUNT_EXPIRATION_TIME);
453    }
454    catch (Exception e)
455    {
456      logger.traceException(e, "User %s is considered to have an expired account because an error occurred " +
457          "while attempting to make the determination.", userDNString);
458
459      isAccountExpired = ConditionResult.TRUE;
460      return true;
461    }
462
463    if (accountExpirationTime > currentTime)
464    {
465      // The user does have an expiration time, but it hasn't arrived yet.
466      isAccountExpired = ConditionResult.FALSE;
467      logger.trace("The account for user %s is not expired because the expiration time has not yet arrived.",
468          userDNString);
469    }
470    else if (accountExpirationTime >= 0)
471    {
472      // The user does have an expiration time, and it is in the past.
473      isAccountExpired = ConditionResult.TRUE;
474      logger.trace("The account for user %s is expired because the expiration time in that account has passed.",
475          userDNString);
476    }
477    else
478    {
479      // The user doesn't have an expiration time in their entry, so it can't be expired.
480      isAccountExpired = ConditionResult.FALSE;
481      logger.trace("The account for user %s is not expired because there is no expiration time in the user's entry.",
482          userDNString);
483    }
484
485    return isAccountExpired == ConditionResult.TRUE;
486  }
487
488  /**
489   * Retrieves the time at which the user's account will expire.
490   *
491   * @return  The time at which the user's account will expire, or -1 if it is not configured with an expiration time.
492   */
493  public long getAccountExpirationTime()
494  {
495    if (accountExpirationTime == Long.MIN_VALUE)
496    {
497      isAccountExpired();
498    }
499
500    return accountExpirationTime;
501  }
502
503  /**
504   * Sets the user's account expiration time to the specified value.
505   *
506   * @param  accountExpirationTime  The time that the user's account should expire.
507   */
508  public void setAccountExpirationTime(long accountExpirationTime)
509  {
510    if (accountExpirationTime < 0)
511    {
512      clearAccountExpirationTime();
513    }
514    else
515    {
516      String timeStr = GeneralizedTimeSyntax.format(accountExpirationTime);
517      logger.trace("Setting account expiration time for user %s to %s", userDNString, timeStr);
518
519      this.accountExpirationTime = accountExpirationTime;
520      replaceAttribute(OP_ATTR_ACCOUNT_EXPIRATION_TIME, timeStr);
521    }
522  }
523
524  /** Clears the user's account expiration time. */
525  public void clearAccountExpirationTime()
526  {
527    logger.trace("Clearing account expiration time for user %s", userDNString);
528
529    accountExpirationTime = -1;
530    clearAttribute(OP_ATTR_ACCOUNT_EXPIRATION_TIME);
531  }
532
533  /**
534   * Retrieves the set of times of failed authentication attempts for the user. If authentication failure
535   * time expiration is enabled, and there are expired times in the entry, these times are removed
536   * from the instance field and an update is provided to delete those values from the entry.
537   *
538   * @return The set of times of failed authentication attempts for the user, which will be an empty list
539   *         in the case of no valid (unexpired) times in the entry.
540   */
541  public List<Long> getAuthFailureTimes()
542  {
543    if (authFailureTimes != null)
544    {
545      if (logger.isTraceEnabled())
546      {
547        logger.trace("Returning stored auth failure time list of %d elements for user %s",
548            authFailureTimes.size(), userDNString);
549      }
550
551      return authFailureTimes;
552    }
553
554    AttributeType type = DirectoryServer.getSchema().getAttributeType(OP_ATTR_PWPOLICY_FAILURE_TIME);
555    try
556    {
557      authFailureTimes = getGeneralizedTimes(type);
558    }
559    catch (Exception e)
560    {
561      logger.traceException(e, "Error while processing auth failure times for user %s", userDNString);
562
563      authFailureTimes = new ArrayList<>();
564      clearAttribute(type);
565      return authFailureTimes;
566    }
567
568    if (authFailureTimes.isEmpty())
569    {
570      if (logger.isTraceEnabled())
571      {
572        logger.trace("Returning an empty auth failure time list for user %s because the attribute" +
573                " is absent from the entry.", userDNString);
574      }
575
576      return authFailureTimes;
577    }
578
579    // Remove any expired failures from the list.
580    if (passwordPolicy.getLockoutFailureExpirationInterval() > 0)
581    {
582      LinkedHashSet<ByteString> valuesToRemove = null;
583
584      long expirationTime = currentTime - passwordPolicy.getLockoutFailureExpirationInterval() * 1000L;
585      Iterator<Long> iterator = authFailureTimes.iterator();
586      while (iterator.hasNext())
587      {
588        long l = iterator.next();
589        if (l < expirationTime)
590        {
591          if (logger.isTraceEnabled())
592          {
593            logger.trace("Removing expired auth failure time %d for user %s", l, userDNString);
594          }
595
596          iterator.remove();
597
598          if (valuesToRemove == null)
599          {
600            valuesToRemove = new LinkedHashSet<>();
601          }
602
603          valuesToRemove.add(ByteString.valueOfUtf8(GeneralizedTimeSyntax.format(l)));
604        }
605      }
606
607      if (valuesToRemove != null)
608      {
609        Attribute a = newAttribute(type, valuesToRemove);
610        modifications.add(new Modification(ModificationType.DELETE, a, true));
611      }
612    }
613
614    if (logger.isTraceEnabled())
615    {
616      logger.trace("Returning auth failure time list of %d elements for user %s",
617          authFailureTimes.size(), userDNString);
618    }
619
620    return authFailureTimes;
621  }
622
623  /**
624   * Updates the set of authentication failure times to include the current time.
625   * If the number of failures reaches the policy configuration limit, lock the account.
626   */
627  public void updateAuthFailureTimes()
628  {
629    if (passwordPolicy.getLockoutFailureCount() <= 0)
630    {
631      return;
632    }
633
634    if (logger.isTraceEnabled())
635    {
636      logger.trace("Updating authentication failure times for user %s", userDNString);
637    }
638
639    List<Long> failureTimes = getAuthFailureTimes();
640    long highestFailureTime = computeHighestTime(failureTimes);
641    // Update the current policy state
642    failureTimes.add(highestFailureTime);
643
644    // And the attribute in the user entry
645    AttributeType type = DirectoryServer.getSchema().getAttributeType(OP_ATTR_PWPOLICY_FAILURE_TIME);
646    Attribute addAttr = Attributes.create(type, GeneralizedTimeSyntax.format(highestFailureTime));
647    modifications.add(new Modification(ModificationType.ADD, addAttr, true));
648
649    // Now check to see if there have been sufficient failures to lock the account.
650    int lockoutCount = passwordPolicy.getLockoutFailureCount();
651    if (lockoutCount > 0 && lockoutCount <= authFailureTimes.size())
652    {
653      setFailureLockedTime(highestFailureTime);
654      if (logger.isTraceEnabled())
655      {
656        logger.trace("Locking user account %s due to too many failures.", userDNString);
657      }
658    }
659  }
660
661  /**
662   * Explicitly specifies the auth failure times for the associated user.  This should generally only be used
663   * for testing purposes.  Note that it will also set or clear the locked time as appropriate.
664   *
665   * @param  authFailureTimes  The set of auth failure times to use for the account.  An empty list or
666   *                           {@code null} will clear the account of any existing failures.
667   */
668  public void setAuthFailureTimes(List<Long> authFailureTimes)
669  {
670    if (authFailureTimes == null || authFailureTimes.isEmpty())
671    {
672      clearAuthFailureTimes();
673      clearFailureLockedTime();
674      return;
675    }
676
677    this.authFailureTimes = authFailureTimes;
678
679    AttributeBuilder builder = new AttributeBuilder(OP_ATTR_PWPOLICY_FAILURE_TIME_LC);
680    long highestFailureTime = -1;
681
682    for (long l : authFailureTimes)
683    {
684      highestFailureTime = Math.max(l, highestFailureTime);
685      builder.add(GeneralizedTimeSyntax.format(l));
686    }
687    replaceAttribute(builder.toAttribute());
688
689    // Now check to see if there have been sufficient failures to lock the account.
690    int lockoutCount = passwordPolicy.getLockoutFailureCount();
691    if (lockoutCount > 0 && lockoutCount <= authFailureTimes.size())
692    {
693      setFailureLockedTime(highestFailureTime);
694      if (logger.isTraceEnabled())
695      {
696        logger.trace("Locking user account %s due to too many failures.", userDNString);
697      }
698    }
699  }
700
701  /** Updates the user entry to remove any record of previous authentication failure times. */
702  private void clearAuthFailureTimes()
703  {
704    logger.trace("Clearing authentication failure times for user %s", userDNString);
705
706    List<Long> failureTimes = getAuthFailureTimes();
707    if (!failureTimes.isEmpty())
708    {
709      failureTimes.clear(); // Note: failureTimes != this.authFailureTimes
710      clearAttribute(OP_ATTR_PWPOLICY_FAILURE_TIME);
711    }
712  }
713
714  /**
715   * Retrieves the time of an authentication failure lockout for the user.
716   *
717   * @return  The time of an authentication failure lockout for the user, or -1 if no such time is present in the entry.
718   */
719  private long getFailureLockedTime()
720  {
721    if (failureLockedTime != Long.MIN_VALUE)
722    {
723      return failureLockedTime;
724    }
725
726    AttributeType type = DirectoryServer.getSchema().getAttributeType(OP_ATTR_PWPOLICY_LOCKED_TIME);
727    try
728    {
729      failureLockedTime = getGeneralizedTime(userEntry, type);
730    }
731    catch (Exception e)
732    {
733      logger.traceException(e, "Returning current time for user %s because an error occurred", userDNString);
734
735      failureLockedTime = currentTime;
736      return failureLockedTime;
737    }
738
739    // An expired locked time is handled in lockedDueToFailures.
740    return failureLockedTime;
741  }
742
743  /**
744    Sets the failure lockout attribute in the entry to the requested time.
745
746    @param time  The time to which to set the entry's failure lockout attribute.
747   */
748  private void setFailureLockedTime(final long time)
749  {
750    if (time != getFailureLockedTime())
751    {
752      failureLockedTime = time;
753      replaceAttribute(OP_ATTR_PWPOLICY_LOCKED_TIME, GeneralizedTimeSyntax.format(failureLockedTime));
754    }
755  }
756
757  /** Updates the user entry to remove any record of previous authentication failure lockout. */
758  private void clearFailureLockedTime()
759  {
760    logger.trace("Clearing failure lockout time for user %s.", userDNString);
761
762    if (-1L != getFailureLockedTime())
763    {
764      failureLockedTime = -1L;
765      clearAttribute(OP_ATTR_PWPOLICY_LOCKED_TIME);
766    }
767  }
768
769  /**
770   * Indicates whether the associated user should be considered locked out as a result of too many
771   * authentication failures. In the case of an expired lock-out, this routine produces the update
772   * to clear the lock-out attribute and the authentication failure timestamps.
773   * In case the failure lockout time is absent from the entry, but sufficient authentication failure
774   * timestamps are present in the entry, this routine produces the update to set the lock-out attribute.
775   *
776   * @return  <CODE>true</CODE> if the user is currently locked out due to too many authentication failures,
777   *          or <CODE>false</CODE> if not.
778   */
779  public boolean lockedDueToFailures()
780  {
781    // FIXME: Introduce a state field to cache the computed value of this method.
782    // Note that only a cached "locked" status can be returned due to the possibility of intervening updates to
783    // this.failureLockedTime by updateAuthFailureTimes.
784
785    // Check if the feature is enabled in the policy.
786    final int maxFailures = passwordPolicy.getLockoutFailureCount();
787    if (maxFailures <= 0)
788    {
789      if (logger.isTraceEnabled())
790      {
791        logger.trace("Returning false for user %s because lockout due to failures is not enabled.", userDNString);
792      }
793
794      return false;
795    }
796
797    // Get the locked time from the user's entry. If it is present and not expired, the account is locked.
798    // If it is absent, the failure timestamps must be checked, since failure timestamps sufficient to lock the
799    // account could be produced across the synchronization topology within the synchronization latency.
800    // Also, note that IETF draft-behera-ldap-password-policy-09 specifies "19700101000000Z" as the value to be set
801    // under a "locked until reset" regime; however, this implementation accepts the value as a locked entry,
802    // but observes the lockout expiration policy for all values including this one.
803    // FIXME: This "getter" is unusual in that it might produce an update to the entry in two cases.
804    // Does it make sense to factor the methods so that, e.g., an expired lockout is reported, and clearing
805    // the lockout is left to the caller?
806    if (getFailureLockedTime() < 0L)
807    {
808      // There was no locked time present in the entry; however, sufficient failure times might have accumulated
809      // to trigger a lockout.
810      if (getAuthFailureTimes().size() < maxFailures)
811      {
812        if (logger.isTraceEnabled())
813        {
814          logger.trace("Returning false for user %s because there is no locked time.", userDNString);
815        }
816
817        return false;
818      }
819
820      // The account isn't locked but should be, so do so now.
821      setFailureLockedTime(currentTime);// FIXME: set to max(failureTimes)?
822
823      if (logger.isTraceEnabled())
824      {
825        logger.trace("Locking user %s because there were enough existing failures even though there was" +
826                " no account locked time.", userDNString);
827      }
828      // Fall through...
829    }
830
831    // There is a failure locked time, but it may be expired.
832    if (passwordPolicy.getLockoutDuration() > 0)
833    {
834      final long unlockTime = getFailureLockedTime() + 1000L * passwordPolicy.getLockoutDuration();
835      if (unlockTime > currentTime)
836      {
837        secondsUntilUnlock = (int) ((unlockTime - currentTime) / 1000);
838
839        if (logger.isTraceEnabled())
840        {
841          logger.trace("Returning true for user %s because there is a locked time and the lockout duration has" +
842                  " not been reached.", userDNString);
843        }
844
845        return true;
846      }
847
848      // The lockout in the entry has expired...
849      clearFailureLockout();
850
851      if (logger.isTraceEnabled())
852      {
853        logger.trace("Returning false for user %s because the existing lockout has expired.", userDNString);
854      }
855
856      assert -1L == getFailureLockedTime();
857      return false;
858    }
859
860    if (logger.isTraceEnabled())
861    {
862      logger.trace("Returning true for user %s because there is a locked time and no lockout duration.", userDNString);
863    }
864
865    assert -1L <= getFailureLockedTime();
866    return true;
867  }
868
869  /**
870   * Retrieves the length of time in seconds until the user's account is automatically unlocked.
871   * This should only be called after calling <CODE>lockedDueToFailures</CODE>.
872   *
873   * @return  The length of time in seconds until the user's account is automatically unlocked, or -1 if the account
874   * is not locked or the lockout requires administrative action to clear.
875   */
876  public int getSecondsUntilUnlock()
877  {
878    // secondsUntilUnlock is only set when failureLockedTime is present and PasswordPolicy.getLockoutDuration
879    // is enabled; hence it is not unreasonable to find secondsUntilUnlock uninitialized.
880    assert failureLockedTime != Long.MIN_VALUE;
881
882    return secondsUntilUnlock < 0 ? -1 : secondsUntilUnlock;
883  }
884
885  /**
886   * Updates the user account to remove any record of a previous lockout due to failed authentications.
887   */
888  public void clearFailureLockout()
889  {
890    clearAuthFailureTimes();
891    clearFailureLockedTime();
892  }
893
894  /**
895   * Retrieves the time that the user last authenticated to the Directory Server.
896   *
897   * @return  The time that the user last authenticated to the Directory Server, or -1 if it cannot be determined.
898   */
899  public long getLastLoginTime()
900  {
901    if (lastLoginTime != Long.MIN_VALUE)
902    {
903      if (logger.isTraceEnabled())
904      {
905        logger.trace("Returning stored last login time of %d for user %s.", lastLoginTime, userDNString);
906      }
907
908      return lastLoginTime;
909    }
910
911    // The policy configuration must be checked since the entry cannot be evaluated without both an attribute
912    // name and timestamp format.
913    AttributeType type   = passwordPolicy.getLastLoginTimeAttribute();
914    String        format = passwordPolicy.getLastLoginTimeFormat();
915
916    if (type == null || format == null)
917    {
918      lastLoginTime = -1;
919      if (logger.isTraceEnabled())
920      {
921        logger.trace("Returning -1 for user %s because no last login time will be maintained.", userDNString);
922      }
923
924      return lastLoginTime;
925    }
926
927    boolean isGeneralizedTime = SYNTAX_GENERALIZED_TIME_NAME.equals(type.getSyntax().getName());
928    lastLoginTime = -1;
929    for (Attribute a : userEntry.getAttribute(type))
930    {
931      if (a.isEmpty())
932      {
933        continue;
934      }
935
936      String valueString = a.iterator().next().toString();
937      try
938      {
939        lastLoginTime = parseTime(format, valueString, isGeneralizedTime);
940
941        if (logger.isTraceEnabled())
942        {
943          logger.trace("Returning last login time of %d for user %s, decoded using current last login time format.",
944              lastLoginTime, userDNString);
945        }
946
947        return lastLoginTime;
948      }
949      catch (Exception e)
950      {
951        logger.traceException(e);
952
953        // This could mean that the last login time was encoded using a previous format.
954        for (String f : passwordPolicy.getPreviousLastLoginTimeFormats())
955        {
956          try
957          {
958            lastLoginTime = parseTime(f, valueString, isGeneralizedTime);
959
960            if (logger.isTraceEnabled())
961            {
962              logger.trace("Returning last login time of %d for user %s decoded using previous last login time "
963                  + "format of %s.", lastLoginTime, userDNString, f);
964            }
965
966            return lastLoginTime;
967          }
968          catch (Exception e2)
969          {
970            logger.traceException(e);
971          }
972        }
973
974        assert lastLoginTime == -1;
975        if (logger.isTraceEnabled())
976        {
977          logger.trace("Returning -1 for user %s because the last login time value %s could not be parsed "
978              + "using any known format.", userDNString, valueString);
979        }
980
981        return lastLoginTime;
982      }
983    }
984
985    assert lastLoginTime == -1;
986    if (logger.isTraceEnabled())
987    {
988      logger.trace("Returning %d for user %s because no last login time value exists.", lastLoginTime, userDNString);
989    }
990
991    return lastLoginTime;
992  }
993
994  private long parseTime(String format, String time, boolean isGeneralizedTime) throws ParseException
995  {
996    SimpleDateFormat dateFormat = new SimpleDateFormat(format);
997    if (isGeneralizedTime)
998    {
999      dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
1000    }
1001    return dateFormat.parse(time).getTime();
1002  }
1003
1004  /** Updates the user entry to set the current time as the last login time. */
1005  public void setLastLoginTime()
1006  {
1007    setLastLoginTime(currentTime);
1008  }
1009
1010  /**
1011   * Updates the user entry to use the specified last login time.  This should be used primarily for testing purposes,
1012   * as the variant that uses the current time should be used most of the time.
1013   *
1014   * @param  lastLoginTime  The last login time to set in the user entry.
1015   */
1016  public void setLastLoginTime(long lastLoginTime)
1017  {
1018    AttributeType type = passwordPolicy.getLastLoginTimeAttribute();
1019    String format = passwordPolicy.getLastLoginTimeFormat();
1020
1021    if (type == null || format == null)
1022    {
1023      return;
1024    }
1025
1026    String timestamp;
1027    try
1028    {
1029      SimpleDateFormat dateFormat = new SimpleDateFormat(format);
1030      // If the attribute has a Generalized Time syntax, make it UTC time.
1031      if (SYNTAX_GENERALIZED_TIME_NAME.equals(type.getSyntax().getName()))
1032      {
1033        dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
1034      }
1035      timestamp = dateFormat.format(new Date(lastLoginTime));
1036      this.lastLoginTime = dateFormat.parse(timestamp).getTime();
1037    }
1038    catch (Exception e)
1039    {
1040      logger.traceException(e, "Unable to set last login time for user %s because an error occurred", userDNString);
1041      return;
1042    }
1043
1044    String existingTimestamp = getValue(type);
1045    if (existingTimestamp != null && timestamp.equals(existingTimestamp))
1046    {
1047      logger.trace("Not updating last login time for user %s because the new value matches the existing value.",
1048          userDNString);
1049      return;
1050    }
1051
1052    replaceAttribute(Attributes.create(type, timestamp));
1053
1054    logger.trace("Updated the last login time for user %s to %s", userDNString, timestamp);
1055  }
1056
1057  /**
1058   * Clears the last login time from the user's entry.  This should generally be used only for testing purposes.
1059   */
1060  public void clearLastLoginTime()
1061  {
1062    logger.trace("Clearing last login time for user %s", userDNString);
1063
1064    lastLoginTime = -1;
1065    clearAttribute(OP_ATTR_LAST_LOGIN_TIME);
1066  }
1067
1068  /**
1069   * Indicates whether the user's account is currently locked because it has been idle for too long.
1070   *
1071   * @return  <CODE>true</CODE> if the user's account is locked because it has been idle for too long,
1072   *          or <CODE>false</CODE> if not.
1073   */
1074  public boolean lockedDueToIdleInterval()
1075  {
1076    if (isIdleLocked != ConditionResult.UNDEFINED)
1077    {
1078      if (logger.isTraceEnabled())
1079      {
1080        logger.trace("Returning stored result of %b for user %s", isIdleLocked == ConditionResult.TRUE, userDNString);
1081      }
1082
1083      return isIdleLocked == ConditionResult.TRUE;
1084    }
1085
1086    // Return immediately if this feature is disabled, since the feature is not responsible for any state attribute
1087    // in the entry.
1088    if (passwordPolicy.getIdleLockoutInterval() <= 0)
1089    {
1090      isIdleLocked = ConditionResult.FALSE;
1091
1092      if (logger.isTraceEnabled())
1093      {
1094        logger.trace("Returning false for user %s because no idle lockout interval is defined.", userDNString);
1095      }
1096      return false;
1097    }
1098
1099    long lockTime = currentTime - 1000L * passwordPolicy.getIdleLockoutInterval();
1100    if (lockTime < 0)
1101    {
1102      lockTime = 0;
1103    }
1104
1105    long theLastLoginTime = getLastLoginTime();
1106    if (theLastLoginTime > lockTime || getPasswordChangedTime() > lockTime)
1107    {
1108      isIdleLocked = ConditionResult.FALSE;
1109      if (logger.isTraceEnabled())
1110      {
1111        StringBuilder reason = new StringBuilder();
1112        if(theLastLoginTime > lockTime)
1113        {
1114          reason.append("the last login time is in an acceptable window");
1115        }
1116        else
1117        {
1118          if(theLastLoginTime < 0)
1119          {
1120            reason.append("there is no last login time, but ");
1121          }
1122          reason.append("the password changed time is in an acceptable window");
1123        }
1124        logger.trace("Returning false for user %s because %s.", userDNString, reason);
1125      }
1126    }
1127    else
1128    {
1129      isIdleLocked = ConditionResult.TRUE;
1130      if (logger.isTraceEnabled())
1131      {
1132        String reason = theLastLoginTime < 0
1133            ? "there is no last login time and the password changed time is not in an acceptable window"
1134            : "neither last login time nor password changed time are in an acceptable window";
1135        logger.trace("Returning true for user %s because %s.", userDNString, reason);
1136      }
1137    }
1138
1139    return isIdleLocked == ConditionResult.TRUE;
1140  }
1141
1142/**
1143* Indicates whether the user's password must be changed before any other operation can be performed.
1144*
1145* @return  <CODE>true</CODE> if the user's password must be changed before any other operation can be performed.
1146*/
1147  public boolean mustChangePassword()
1148  {
1149    if(mustChangePassword != ConditionResult.UNDEFINED)
1150    {
1151      if (logger.isTraceEnabled())
1152      {
1153        logger.trace("Returning stored result of %b for user %s.",
1154            mustChangePassword == ConditionResult.TRUE, userDNString);
1155      }
1156
1157      return mustChangePassword == ConditionResult.TRUE;
1158    }
1159
1160    // If the password policy doesn't use force change on add or force change on reset, or if it forbids the user
1161    // from changing his password, then return false.
1162    // FIXME: the only getter responsible for a state attribute (pwdReset) that considers the policy before
1163    // checking the entry for the presence of the attribute.
1164    if (!passwordPolicy.isAllowUserPasswordChanges()
1165        || (!passwordPolicy.isForceChangeOnAdd() && !passwordPolicy.isForceChangeOnReset()))
1166    {
1167      mustChangePassword = ConditionResult.FALSE;
1168      if (logger.isTraceEnabled())
1169      {
1170        logger.trace("Returning false for user %s because neither force change on add nor force change on reset" +
1171                " is enabled, or users are not allowed to self-modify passwords.", userDNString);
1172      }
1173
1174      return false;
1175    }
1176
1177    AttributeType type = DirectoryServer.getSchema().getAttributeType(OP_ATTR_PWPOLICY_RESET_REQUIRED);
1178    try
1179    {
1180      mustChangePassword = getBoolean(userEntry, type);
1181    }
1182    catch (Exception e)
1183    {
1184      logger.traceException(e, "Returning true for user %s because an error occurred", userDNString);
1185      mustChangePassword = ConditionResult.TRUE;
1186      return true;
1187    }
1188
1189    if(mustChangePassword == ConditionResult.UNDEFINED)
1190    {
1191      mustChangePassword = ConditionResult.FALSE;
1192      logger.trace("Returning %b for user since the attribute \"%s\" is not present in the entry.",
1193          false, userDNString, OP_ATTR_PWPOLICY_RESET_REQUIRED);
1194      return false;
1195    }
1196
1197    final boolean result = mustChangePassword == ConditionResult.TRUE;
1198    logger.trace("Returning %b for user %s.", result, userDNString);
1199    return result;
1200  }
1201
1202/**
1203* Updates the user entry to indicate whether the user's password must be changed.
1204*
1205* @param  mustChangePassword  Indicates whether the user's password must be changed.
1206*/
1207  public void setMustChangePassword(boolean mustChangePassword)
1208  {
1209    if (logger.isTraceEnabled())
1210    {
1211      logger.trace("Updating user %s to set the reset flag to %b", userDNString, mustChangePassword);
1212    }
1213
1214    if (mustChangePassword == mustChangePassword())
1215    {
1216      return;  // requested state matches current state
1217    }
1218
1219    this.mustChangePassword = ConditionResult.not(this.mustChangePassword);
1220    replaceAttribute(OP_ATTR_PWPOLICY_RESET_REQUIRED, mustChangePassword);
1221  }
1222
1223  private void replaceAttribute(String attrName, boolean newValue)
1224  {
1225    if (newValue)
1226    {
1227      replaceAttribute(attrName, String.valueOf(true));
1228    }
1229    else
1230    {
1231      clearAttribute(attrName);
1232    }
1233  }
1234
1235  private void clearAttribute(String attrName)
1236  {
1237    clearAttribute(DirectoryServer.getSchema().getAttributeType(attrName));
1238  }
1239
1240  private void clearAttribute(AttributeType type)
1241  {
1242    replaceAttribute(Attributes.empty(type));
1243  }
1244
1245  private void replaceAttribute(String attrName, String attrValue)
1246  {
1247    replaceAttribute(Attributes.create(attrName, attrValue));
1248  }
1249
1250  private void replaceAttribute(Attribute a)
1251  {
1252    modifications.add(new Modification(ModificationType.REPLACE, a, true));
1253  }
1254
1255  /**
1256   * Indicates whether the user's account is locked because the password has been reset by an administrator
1257   * but the user did not change the password in a timely manner.
1258   *
1259   * @return  <CODE>true</CODE> if the user's account is locked because of the maximum reset age,
1260   *          or <CODE>false</CODE> if not.
1261   */
1262  public boolean lockedDueToMaximumResetAge()
1263  {
1264    // This feature is responsible for neither a state field nor an entry state attribute.
1265    if (passwordPolicy.getMaxPasswordResetAge() <= 0L)
1266    {
1267      if (logger.isTraceEnabled())
1268      {
1269        logger.trace("Returning false for user %s because there is no maximum reset age.", userDNString);
1270      }
1271
1272      return false;
1273    }
1274
1275    if (! mustChangePassword())
1276    {
1277      if (logger.isTraceEnabled())
1278      {
1279        logger.trace("Returning false for user %s because the user's password has not been reset.", userDNString);
1280      }
1281
1282      return false;
1283    }
1284
1285    long maxResetTime = getPasswordChangedTime() + 1000L * passwordPolicy.getMaxPasswordResetAge();
1286    boolean locked = maxResetTime < currentTime;
1287
1288    if (logger.isTraceEnabled())
1289    {
1290      logger.trace("Returning %b for user %s after comparing the current and max reset times.", locked, userDNString);
1291    }
1292
1293    return locked;
1294  }
1295
1296  /**
1297   * Returns whether the account was locked for any reason.
1298   *
1299   * @return true if the account is locked, false otherwise
1300   */
1301  public boolean isLocked()
1302  {
1303    return lockedDueToIdleInterval() || lockedDueToMaximumResetAge() || lockedDueToFailures();
1304  }
1305
1306  /**
1307   * Retrieves the time that the user's password should expire (if the expiration is in the future) or
1308   * did expire (if the expiration was in the past).  Note that this method should be called after the
1309   * <CODE>lockedDueToMaximumResetAge</CODE> method because grace logins will not be allowed in the case
1310   * that the maximum reset age has passed whereas they may be used for expiration due to maximum password
1311   * age or forced change time.
1312   *
1313   * @return  The time that the user's password should/did expire, or -1 if it should not expire.
1314   */
1315  public long getPasswordExpirationTime()
1316  {
1317    if (passwordExpirationTime == Long.MIN_VALUE)
1318    {
1319      passwordExpirationTime = Long.MAX_VALUE;
1320
1321      boolean checkWarning = false;
1322
1323      long maxAge = passwordPolicy.getMaxPasswordAge();
1324      if (maxAge > 0L)
1325      {
1326        long expTime = getPasswordChangedTime() + 1000L * maxAge;
1327        if (expTime < passwordExpirationTime)
1328        {
1329          passwordExpirationTime = expTime;
1330          checkWarning   = true;
1331        }
1332      }
1333
1334      long maxResetAge = passwordPolicy.getMaxPasswordResetAge();
1335      if (mustChangePassword() && maxResetAge > 0L)
1336      {
1337        long expTime = getPasswordChangedTime() + 1000L * maxResetAge;
1338        if (expTime < passwordExpirationTime)
1339        {
1340          passwordExpirationTime = expTime;
1341          checkWarning   = false;
1342        }
1343      }
1344
1345      long mustChangeTime = passwordPolicy.getRequireChangeByTime();
1346      if (mustChangeTime > 0)
1347      {
1348        long reqChangeTime = getRequiredChangeTime();
1349        if (reqChangeTime != mustChangeTime && mustChangeTime < passwordExpirationTime)
1350        {
1351          passwordExpirationTime = mustChangeTime;
1352          checkWarning   = true;
1353        }
1354      }
1355
1356      if (passwordExpirationTime == Long.MAX_VALUE)
1357      {
1358        passwordExpirationTime = -1;
1359        shouldWarn             = ConditionResult.FALSE;
1360        isFirstWarning         = ConditionResult.FALSE;
1361        isPasswordExpired      = ConditionResult.FALSE;
1362        mayUseGraceLogin       = ConditionResult.TRUE;
1363      }
1364      else if (checkWarning)
1365      {
1366        mayUseGraceLogin = ConditionResult.TRUE;
1367
1368        long warningInterval = passwordPolicy.getPasswordExpirationWarningInterval();
1369        if (warningInterval > 0L)
1370        {
1371          long shouldWarnTime = passwordExpirationTime - warningInterval * 1000L;
1372          if (shouldWarnTime > currentTime)
1373          {
1374            // The warning time is in the future, so we know the password isn't expired.
1375            shouldWarn        = ConditionResult.FALSE;
1376            isFirstWarning    = ConditionResult.FALSE;
1377            isPasswordExpired = ConditionResult.FALSE;
1378          }
1379          else
1380          {
1381            // We're at least in the warning period, but the password may be expired.
1382            long theWarnedTime = getWarnedTime();
1383
1384            if (passwordExpirationTime > currentTime)
1385            {
1386              // The password is not expired but we should warn the user.
1387              shouldWarn        = ConditionResult.TRUE;
1388              isPasswordExpired = ConditionResult.FALSE;
1389
1390              if (theWarnedTime < 0)
1391              {
1392                isFirstWarning = ConditionResult.TRUE;
1393                setWarnedTime();
1394
1395                if (! passwordPolicy.isExpirePasswordsWithoutWarning())
1396                {
1397                  passwordExpirationTime = currentTime + warningInterval * 1000L;
1398                }
1399              }
1400              else
1401              {
1402                isFirstWarning = ConditionResult.FALSE;
1403
1404                if (! passwordPolicy.isExpirePasswordsWithoutWarning())
1405                {
1406                  passwordExpirationTime = theWarnedTime + warningInterval * 1000L;
1407                }
1408              }
1409            }
1410            else
1411            {
1412              // The expiration time has passed, but we may not actually be expired if the user has not
1413              // yet seen a warning.
1414              if (passwordPolicy.isExpirePasswordsWithoutWarning())
1415              {
1416                shouldWarn        = ConditionResult.FALSE;
1417                isFirstWarning    = ConditionResult.FALSE;
1418                isPasswordExpired = ConditionResult.TRUE;
1419              }
1420              else if (theWarnedTime > 0)
1421              {
1422                passwordExpirationTime = theWarnedTime + warningInterval*1000L;
1423                if (passwordExpirationTime > currentTime)
1424                {
1425                  shouldWarn        = ConditionResult.TRUE;
1426                  isFirstWarning    = ConditionResult.FALSE;
1427                  isPasswordExpired = ConditionResult.FALSE;
1428                }
1429                else
1430                {
1431                  shouldWarn        = ConditionResult.FALSE;
1432                  isFirstWarning    = ConditionResult.FALSE;
1433                  isPasswordExpired = ConditionResult.TRUE;
1434                }
1435              }
1436              else
1437              {
1438                shouldWarn             = ConditionResult.TRUE;
1439                isFirstWarning         = ConditionResult.TRUE;
1440                isPasswordExpired      = ConditionResult.FALSE;
1441                passwordExpirationTime = currentTime + warningInterval*1000L;
1442              }
1443            }
1444          }
1445        }
1446        else
1447        {
1448          // There will never be a warning, and the user's password may be expired.
1449          shouldWarn     = ConditionResult.FALSE;
1450          isFirstWarning = ConditionResult.FALSE;
1451          isPasswordExpired = ConditionResult.valueOf(currentTime > passwordExpirationTime);
1452        }
1453      }
1454      else
1455      {
1456        mayUseGraceLogin = ConditionResult.FALSE;
1457        shouldWarn       = ConditionResult.FALSE;
1458        isFirstWarning   = ConditionResult.FALSE;
1459        isPasswordExpired = ConditionResult.valueOf(passwordExpirationTime < currentTime);
1460      }
1461    }
1462
1463    if (logger.isTraceEnabled())
1464    {
1465      logger.trace("Returning password expiration time of %d for user %s.", passwordExpirationTime, userDNString);
1466    }
1467
1468    return passwordExpirationTime;
1469  }
1470
1471  /**
1472   * Indicates whether the user's password is currently expired.
1473   *
1474   * @return  <CODE>true</CODE> if the user's password is currently expired, or <CODE>false</CODE> if not.
1475   */
1476  public boolean isPasswordExpired()
1477  {
1478    refreshIfUndefined(isPasswordExpired);
1479    return isPasswordExpired == ConditionResult.TRUE;
1480  }
1481
1482  /**
1483   * Indicates whether the user's last password change was within the minimum password age.
1484   *
1485   * @return  <CODE>true</CODE> if the password minimum age is nonzero, the account is not in force-change mode,
1486   *          and the last password change was within the minimum age, or <CODE>false</CODE> otherwise.
1487   */
1488  public boolean isWithinMinimumAge()
1489  {
1490    // This feature is responsible for neither a state field nor entry state attribute.
1491    long minAge = passwordPolicy.getMinPasswordAge();
1492    if (minAge <= 0L)
1493    {
1494      // There is no minimum age, so the user isn't in it.
1495      if (logger.isTraceEnabled())
1496      {
1497        logger.trace("Returning false because there is no minimum age.");
1498      }
1499
1500      return false;
1501    }
1502    else if (getPasswordChangedTime() + minAge * 1000L < currentTime)
1503    {
1504      // It's been long enough since the user changed their password.
1505      if (logger.isTraceEnabled())
1506      {
1507        logger.trace("Returning false because the minimum age has expired.");
1508      }
1509
1510      return false;
1511    }
1512    else if (mustChangePassword())
1513    {
1514      // The user is in a must-change mode, so the minimum age doesn't apply.
1515      if (logger.isTraceEnabled())
1516      {
1517        logger.trace("Returning false because the account is in a must-change state.");
1518      }
1519
1520      return false;
1521    }
1522    else
1523    {
1524      // The user is within the minimum age.
1525      if (logger.isTraceEnabled())
1526      {
1527        logger.trace("Returning true.");
1528      }
1529
1530      return true;
1531    }
1532  }
1533
1534  /**
1535   * Indicates whether the user may use a grace login if the password is expired and there is at least one
1536   * grace login remaining.  Note that this does not check to see if the user's password is expired, does not
1537   * verify that there are any remaining grace logins, and does not update the set of grace login times.
1538   *
1539   * @return  <CODE>true</CODE> if the user may use a grace login if the password is expired and there is
1540   *          at least one grace login remaining, or <CODE>false</CODE> if the user may not use a grace
1541   *          login for some reason.
1542   */
1543  public boolean mayUseGraceLogin()
1544  {
1545    refreshIfUndefined(mayUseGraceLogin);
1546    return mayUseGraceLogin == ConditionResult.TRUE;
1547  }
1548
1549  /**
1550   * Indicates whether the user should receive a warning notification that the password is about to expire.
1551   *
1552   * @return  <CODE>true</CODE> if the user should receive a warning notification that the password is about to expire,
1553   *          or <CODE>false</CODE> if not.
1554   */
1555  public boolean shouldWarn()
1556  {
1557    refreshIfUndefined(shouldWarn);
1558    return shouldWarn == ConditionResult.TRUE;
1559  }
1560
1561  /**
1562   * Indicates whether the warning that the user should receive would be the first warning for the user.
1563   *
1564   * @return  <CODE>true</CODE> if the warning that should be sent to the user would be the first warning,
1565   *          or <CODE>false</CODE> if not.
1566   */
1567  public boolean isFirstWarning()
1568  {
1569    refreshIfUndefined(isFirstWarning);
1570    return isFirstWarning == ConditionResult.TRUE;
1571  }
1572
1573  private void refreshIfUndefined(ConditionResult cond)
1574  {
1575    if (cond == null || cond == ConditionResult.UNDEFINED)
1576    {
1577      getPasswordExpirationTime();
1578    }
1579  }
1580
1581  /**
1582   * Retrieves the length of time in seconds until the user's password expires.
1583   *
1584   * @return  The length of time in seconds until the user's password expires,
1585   *          0 if the password is currently expired, or -1 if the password should not expire.
1586   */
1587  public int getSecondsUntilExpiration()
1588  {
1589    long expirationTime = getPasswordExpirationTime();
1590    if (expirationTime < 0)
1591    {
1592      return -1;
1593    }
1594    else if (expirationTime < currentTime)
1595    {
1596      return 0;
1597    }
1598    else
1599    {
1600      return (int) ((expirationTime - currentTime) / 1000);
1601    }
1602  }
1603
1604  /**
1605   * Retrieves the timestamp for the last required change time that the user complied with.
1606   *
1607   * @return  The timestamp for the last required change time that the user complied with,
1608   *          or -1 if the user's password has not been changed in compliance with this configuration.
1609   */
1610  public long getRequiredChangeTime()
1611  {
1612    if (requiredChangeTime != Long.MIN_VALUE)
1613    {
1614      if (logger.isTraceEnabled())
1615      {
1616        logger.trace("Returning stored required change time of %d for user %s", requiredChangeTime, userDNString);
1617      }
1618
1619      return requiredChangeTime;
1620    }
1621
1622    try
1623    {
1624      requiredChangeTime = getGeneralizedTime0(userEntry, OP_ATTR_PWPOLICY_CHANGED_BY_REQUIRED_TIME);
1625    }
1626    catch (Exception e)
1627    {
1628      logger.traceException(e, "Returning %d for user %s because an error occurred", requiredChangeTime, userDNString);
1629
1630      requiredChangeTime = -1;
1631      return requiredChangeTime;
1632    }
1633
1634    logger.trace("Returning required change time of %d for user %s", requiredChangeTime, userDNString);
1635
1636    return requiredChangeTime;
1637  }
1638
1639  /**
1640   * Updates the user entry with a timestamp indicating that the password has been changed in accordance
1641   * with the require change time.
1642   */
1643  public void setRequiredChangeTime()
1644  {
1645    long requiredChangeByTimePolicy = passwordPolicy.getRequireChangeByTime();
1646    if (requiredChangeByTimePolicy > 0)
1647    {
1648      setRequiredChangeTime(requiredChangeByTimePolicy);
1649    }
1650  }
1651
1652  /**
1653   * Updates the user entry with a timestamp indicating that the password has been changed in accordance
1654   * with the require change time.
1655   *
1656   * @param  requiredChangeTime  The timestamp to use for the required change time value.
1657   */
1658  public void setRequiredChangeTime(long requiredChangeTime)
1659  {
1660    if (logger.isTraceEnabled())
1661    {
1662      logger.trace("Updating required change time for user %s", userDNString);
1663    }
1664
1665    if (getRequiredChangeTime() != requiredChangeTime)
1666    {
1667      this.requiredChangeTime = requiredChangeTime;
1668      replaceAttribute(OP_ATTR_PWPOLICY_CHANGED_BY_REQUIRED_TIME, GeneralizedTimeSyntax.format(requiredChangeTime));
1669    }
1670  }
1671
1672  /**
1673   * Updates the user entry to remove any timestamp indicating that the password has been changed in accordance
1674   * with the required change time.
1675   */
1676  public void clearRequiredChangeTime()
1677  {
1678    logger.trace("Clearing required change time for user %s", userDNString);
1679
1680    this.requiredChangeTime = Long.MIN_VALUE;
1681    clearAttribute(OP_ATTR_PWPOLICY_CHANGED_BY_REQUIRED_TIME);
1682  }
1683
1684  /**
1685   * Retrieves the time that the user was first warned about an upcoming expiration.
1686   *
1687   * @return  The time that the user was first warned about an upcoming expiration, or -1 if the user has
1688   *          not been warned.
1689   */
1690  public long getWarnedTime()
1691  {
1692    if (warnedTime == Long.MIN_VALUE)
1693    {
1694      try
1695      {
1696        warnedTime = getGeneralizedTime0(userEntry, OP_ATTR_PWPOLICY_WARNED_TIME);
1697      }
1698      catch (Exception e)
1699      {
1700        logger.traceException(e, "Unable to decode the warned time for user %s", userDNString);
1701        warnedTime = -1;
1702      }
1703    }
1704
1705    logger.trace("Returning a warned time of %d for user %s", warnedTime, userDNString);
1706    return warnedTime;
1707  }
1708
1709  /** Updates the user entry to set the warned time to the current time. */
1710  public void setWarnedTime()
1711  {
1712    setWarnedTime(currentTime);
1713  }
1714
1715  /**
1716   * Updates the user entry to set the warned time to the specified time.  This method should generally
1717   * only be used for testing purposes, since the variant that uses the current time is preferred almost
1718   * everywhere else.
1719   *
1720   * @param  warnedTime  The value to use for the warned time.
1721   */
1722  public void setWarnedTime(long warnedTime)
1723  {
1724    long warnTime = getWarnedTime();
1725    if (warnTime == warnedTime)
1726    {
1727      if (logger.isTraceEnabled())
1728      {
1729        logger.trace("Not updating warned time for user %s because the warned time is the same as the specified time.",
1730            userDNString);
1731      }
1732
1733      return;
1734    }
1735
1736    this.warnedTime = warnedTime;
1737    AttributeType type = DirectoryServer.getSchema().getAttributeType(OP_ATTR_PWPOLICY_WARNED_TIME);
1738    replaceAttribute(Attributes.create(type, GeneralizedTimeSyntax.createGeneralizedTimeValue(currentTime)));
1739
1740    if (logger.isTraceEnabled())
1741    {
1742      logger.trace("Updated the warned time for user %s", userDNString);
1743    }
1744  }
1745
1746  /** Updates the user entry to clear the warned time. */
1747  public void clearWarnedTime()
1748  {
1749    logger.trace("Clearing warned time for user %s", userDNString);
1750
1751    if (getWarnedTime() >= 0)
1752    {
1753      warnedTime = -1;
1754      clearAttribute(OP_ATTR_PWPOLICY_WARNED_TIME);
1755
1756      logger.trace("Cleared the warned time for user %s", userDNString);
1757    }
1758  }
1759
1760  /**
1761   * Retrieves the times that the user has authenticated to the server using a grace login.
1762   *
1763   * @return  The times that the user has authenticated to the server using a grace login.
1764   */
1765  public List<Long> getGraceLoginTimes()
1766  {
1767    if (graceLoginTimes == null)
1768    {
1769      AttributeType type = DirectoryServer.getSchema().getAttributeType(OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME);
1770      try
1771      {
1772        graceLoginTimes = getGeneralizedTimes(type);
1773      }
1774      catch (Exception e)
1775      {
1776        logger.traceException(e, "Error while processing grace login times for user %s", userDNString);
1777
1778        graceLoginTimes = new ArrayList<>();
1779        clearAttribute(type);
1780      }
1781    }
1782
1783    logger.trace("Returning grace login times for user %s", userDNString);
1784    return graceLoginTimes;
1785  }
1786
1787  /**
1788   * Retrieves the number of grace logins that the user has left.
1789   *
1790   * @return  The number of grace logins that the user has left, or -1 if grace logins are not allowed.
1791   */
1792  public int getGraceLoginsRemaining()
1793  {
1794    int maxGraceLogins = passwordPolicy.getGraceLoginCount();
1795    if (maxGraceLogins <= 0)
1796    {
1797      return -1;
1798    }
1799
1800    List<Long> theGraceLoginTimes = getGraceLoginTimes();
1801    return maxGraceLogins - theGraceLoginTimes.size();
1802  }
1803
1804  /** Updates the set of grace login times for the user to include the current time. */
1805  public void updateGraceLoginTimes()
1806  {
1807    if (logger.isTraceEnabled())
1808    {
1809      logger.trace("Updating grace login times for user %s", userDNString);
1810    }
1811
1812    List<Long> graceTimes = getGraceLoginTimes();
1813    long highestGraceTime = computeHighestTime(graceTimes);
1814    graceTimes.add(highestGraceTime); // graceTimes == this.graceLoginTimes
1815
1816    AttributeType type = DirectoryServer.getSchema().getAttributeType(OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME);
1817    Attribute addAttr = Attributes.create(type, GeneralizedTimeSyntax.format(highestGraceTime));
1818    modifications.add(new Modification(ModificationType.ADD, addAttr, true));
1819  }
1820
1821  private long computeHighestTime(List<Long> graceTimes)
1822  {
1823    long highestTime = -1;
1824    for (long l : graceTimes)
1825    {
1826      highestTime = Math.max(l, highestTime);
1827    }
1828
1829    if (highestTime >= currentTime)
1830    {
1831      highestTime++;
1832    }
1833    else
1834    {
1835      highestTime = currentTime;
1836    }
1837    return highestTime;
1838  }
1839
1840  /**
1841   * Specifies the set of grace login use times for the associated user.  If the provided list is empty
1842   * or {@code null}, then the set will be cleared.
1843   *
1844   * @param  graceLoginTimes  The grace login use times for the associated user.
1845   */
1846  public void setGraceLoginTimes(List<Long> graceLoginTimes)
1847  {
1848    if (graceLoginTimes == null || graceLoginTimes.isEmpty())
1849    {
1850      clearGraceLoginTimes();
1851      return;
1852    }
1853
1854    if (logger.isTraceEnabled())
1855    {
1856      logger.trace("Updating grace login times for user %s", userDNString);
1857    }
1858
1859    this.graceLoginTimes = graceLoginTimes;
1860
1861    AttributeBuilder builder = new AttributeBuilder(OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME_LC);
1862    for (long l : graceLoginTimes)
1863    {
1864      builder.add(GeneralizedTimeSyntax.format(l));
1865    }
1866    replaceAttribute(builder.toAttribute());
1867  }
1868
1869  /** Updates the user entry to remove any record of previous grace logins. */
1870  public void clearGraceLoginTimes()
1871  {
1872    logger.trace("Clearing grace login times for user %s", userDNString);
1873
1874    List<Long> graceTimes = getGraceLoginTimes();
1875    if (!graceTimes.isEmpty())
1876    {
1877      graceTimes.clear(); // graceTimes == this.graceLoginTimes
1878      clearAttribute(OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME);
1879    }
1880  }
1881
1882  /**
1883   * Retrieves a list of the clear-text passwords for the user.  If the user does not have any passwords
1884   * in the clear, then the list will be empty.
1885   *
1886   * @return  A list of the clear-text passwords for the user.
1887   */
1888  public List<ByteString> getClearPasswords()
1889  {
1890    final List<Attribute> attrList = userEntry.getAttribute(passwordPolicy.getPasswordAttribute());
1891    if (attrList.isEmpty())
1892    {
1893      return Collections.emptyList();
1894    }
1895
1896    LinkedList<ByteString> clearPasswords = new LinkedList<>();
1897    for (Attribute a : attrList)
1898    {
1899      for (ByteString v : a)
1900      {
1901        try
1902        {
1903          String[] pwComponents = getPwComponents(v);
1904
1905          String schemeName = pwComponents[0];
1906          PasswordStorageScheme<?> scheme = getPasswordStorageScheme(schemeName);
1907          if (scheme == null)
1908          {
1909            if (logger.isTraceEnabled())
1910            {
1911              logger.trace("User entry %s contains a password with scheme %s that is not defined in the server.",
1912                  userDNString, schemeName);
1913            }
1914
1915            continue;
1916          }
1917
1918          if (scheme.isReversible())
1919          {
1920            clearPasswords.add(getPlaintextValue(scheme, pwComponents));
1921          }
1922        }
1923        catch (Exception e)
1924        {
1925          logger.traceException(e);
1926
1927          if (logger.isTraceEnabled())
1928          {
1929            logger.trace("Cannot get clear password value for user %s: %s", userDNString, e);
1930          }
1931        }
1932      }
1933    }
1934
1935    return clearPasswords;
1936  }
1937
1938  private ByteString getPlaintextValue(PasswordStorageScheme<?> scheme, String[] pwComponents)
1939      throws DirectoryException
1940  {
1941    return passwordPolicy.isAuthPasswordSyntax()
1942        ? scheme.getAuthPasswordPlaintextValue(pwComponents[1], pwComponents[2])
1943        : scheme.getPlaintextValue(ByteString.valueOfUtf8(pwComponents[1]));
1944  }
1945
1946  @Override
1947  public boolean passwordMatches(ByteString password)
1948  {
1949    List<Attribute> attrList = userEntry.getAttribute(passwordPolicy.getPasswordAttribute());
1950    if (attrList.isEmpty())
1951    {
1952      if (logger.isTraceEnabled())
1953      {
1954        logger.trace("Returning false because user %s does not have any values for password attribute %s",
1955            userDNString, passwordPolicy.getPasswordAttribute().getNameOrOID());
1956      }
1957
1958      return false;
1959    }
1960
1961    for (Attribute a : attrList)
1962    {
1963      for (ByteString v : a)
1964      {
1965        try
1966        {
1967          String[] pwComponents = getPwComponents(v);
1968          String schemeName = pwComponents[0];
1969          PasswordStorageScheme<?> scheme = getPasswordStorageScheme(schemeName);
1970          if (scheme == null)
1971          {
1972            if (logger.isTraceEnabled())
1973            {
1974              logger.trace("User entry %s contains a password with scheme %s that is not defined in the server.",
1975                                  userDNString, schemeName);
1976            }
1977
1978            continue;
1979          }
1980
1981          if (passwordMatches(password, pwComponents, scheme))
1982          {
1983            if (logger.isTraceEnabled())
1984            {
1985              logger.trace("Returning true for user %s because the provided password matches a value " +
1986                      "encoded with scheme %s", userDNString, schemeName);
1987            }
1988
1989            return true;
1990          }
1991        }
1992        catch (Exception e)
1993        {
1994          logger.traceException(e, "An error occurred while attempting to process a password value for user %s",
1995              userDNString);
1996        }
1997      }
1998    }
1999
2000    // If we've gotten here, then we couldn't find a match.
2001    logger.trace("Returning false because the provided password does not match any of the stored password " +
2002            "values for user %s", userDNString);
2003
2004    return false;
2005  }
2006
2007  /**
2008   * Get the broken-down components of the given password value.
2009   *
2010   * @param  usesAuthPasswordSyntax  true if the value is an authPassword.
2011   * @param  v  The encoded password value to break down.
2012   *
2013   * @return An array of components.
2014   */
2015  private String[] getPwComponents(ByteString v) throws DirectoryException
2016  {
2017    return passwordPolicy.isAuthPasswordSyntax()
2018        ? AuthPasswordSyntax.decodeAuthPassword(v.toString())
2019        : UserPasswordSyntax.decodeUserPassword(v.toString());
2020  }
2021
2022  /**
2023   * Indicates whether the provided password value is pre-encoded.
2024   *
2025   * @param  passwordValue  The value for which to make the determination.
2026   *
2027   * @return  <CODE>true</CODE> if the provided password value is pre-encoded, or <CODE>false</CODE> if it is not.
2028   */
2029  public boolean passwordIsPreEncoded(ByteString passwordValue)
2030  {
2031    return passwordPolicy.isAuthPasswordSyntax()
2032        ? AuthPasswordSyntax.isEncoded(passwordValue)
2033        : UserPasswordSyntax.isEncoded(passwordValue);
2034  }
2035
2036  /**
2037   * Encodes the provided password using the default storage schemes (using the appropriate syntax for the
2038   * password attribute).
2039   *
2040   * @param  password  The password to be encoded.
2041   *
2042   * @return  The password encoded using the default schemes.
2043   *
2044   * @throws  DirectoryException  If a problem occurs while attempting to encode the password.
2045   */
2046  public List<ByteString> encodePassword(ByteString password)
2047         throws DirectoryException
2048  {
2049    List<PasswordStorageScheme<?>> schemes = passwordPolicy.getDefaultPasswordStorageSchemes();
2050    List<ByteString> encodedPasswords = new ArrayList<>(schemes.size());
2051
2052    if (passwordPolicy.isAuthPasswordSyntax())
2053    {
2054      for (PasswordStorageScheme<?> s : schemes)
2055      {
2056        encodedPasswords.add(s.encodeAuthPassword(password));
2057      }
2058    }
2059    else
2060    {
2061      for (PasswordStorageScheme<?> s : schemes)
2062      {
2063        encodedPasswords.add(s.encodePasswordWithScheme(password));
2064      }
2065    }
2066
2067    return encodedPasswords;
2068  }
2069
2070  /**
2071   * Indicates whether the provided password appears to be acceptable according to the password validators.
2072   *
2073   * @param  operation         The operation that provided the password.
2074   * @param  userEntry         The user entry in which the password is used.
2075   * @param  newPassword       The password to be validated.
2076   * @param  currentPasswords  The set of clear-text current passwords for the user (this may be a subset
2077   *                           if not all of them are available in the clear, or empty if none of them
2078   *                           are available in the clear).
2079   * @param  invalidReason     A buffer that may be used to hold the invalid reason if the password is rejected.
2080   *
2081   * @return  <CODE>true</CODE> if the password is acceptable for use, or <CODE>false</CODE> if it is not.
2082   */
2083  public boolean passwordIsAcceptable(Operation operation, Entry userEntry, ByteString newPassword,
2084                                      Set<ByteString> currentPasswords, LocalizableMessageBuilder invalidReason)
2085  {
2086    for (PasswordValidator<?> validator : passwordPolicy.getPasswordValidators())
2087    {
2088      if (!validator.passwordIsAcceptable(newPassword, currentPasswords, operation, userEntry, invalidReason))
2089      {
2090        if (logger.isTraceEnabled())
2091        {
2092          logger.trace("The password provided for user %s failed validation: %s", userDNString, invalidReason);
2093        }
2094        return false;
2095      }
2096    }
2097    return true;
2098  }
2099
2100  /**
2101   * Performs any processing that may be necessary to remove deprecated storage schemes from the user's entry
2102   * that match the provided password and re-encodes them using the default schemes.
2103   *
2104   * @param  password  The clear-text password provided by the user.
2105   */
2106  public void handleDeprecatedStorageSchemes(ByteString password)
2107  {
2108    if (passwordPolicy.getDeprecatedPasswordStorageSchemes().isEmpty())
2109    {
2110      if (logger.isTraceEnabled())
2111      {
2112        logger.trace("Doing nothing for user %s because no deprecated storage schemes have been defined.",
2113            userDNString);
2114      }
2115
2116      return;
2117    }
2118
2119    AttributeType type = passwordPolicy.getPasswordAttribute();
2120    List<Attribute> attrList = userEntry.getAttribute(type);
2121    if (attrList.isEmpty())
2122    {
2123      logger.trace("Doing nothing for entry %s because no password values were found.", userDNString);
2124      return;
2125    }
2126
2127    HashSet<String> existingDefaultSchemes = new HashSet<>();
2128    LinkedHashSet<ByteString> removedValues = new LinkedHashSet<>();
2129    LinkedHashSet<ByteString> updatedValues = new LinkedHashSet<>();
2130
2131    for (Attribute a : attrList)
2132    {
2133      for (ByteString v : a) {
2134        try {
2135          String[] pwComponents = getPwComponents(v);
2136
2137          String schemeName = pwComponents[0];
2138          PasswordStorageScheme<?> scheme = getPasswordStorageScheme(schemeName);
2139          if (scheme == null) {
2140            if (logger.isTraceEnabled()) {
2141              logger.trace("Skipping password value for user %s because the associated storage scheme %s " +
2142                  "is not configured for use.", userDNString, schemeName);
2143            }
2144            continue;
2145          }
2146
2147          if (passwordMatches(password, pwComponents, scheme))
2148          {
2149            if (passwordPolicy.isDefaultPasswordStorageScheme(schemeName)) {
2150              existingDefaultSchemes.add(schemeName);
2151              updatedValues.add(v);
2152            } else if (passwordPolicy.isDeprecatedPasswordStorageScheme(schemeName)) {
2153              if (logger.isTraceEnabled()) {
2154                logger.trace("Marking password with scheme %s for removal from user entry %s.",
2155                    schemeName, userDNString);
2156              }
2157              removedValues.add(v);
2158            } else {
2159              updatedValues.add(v);
2160            }
2161          }
2162        } catch (Exception e) {
2163          logger.traceException(e, "Skipping password value for user %s because an error occurred while attempting " +
2164              "to decode it based on the user password syntax", userDNString);
2165        }
2166      }
2167    }
2168
2169    if (removedValues.isEmpty())
2170    {
2171      logger.trace("User entry %s does not have any password values encoded using deprecated schemes.", userDNString);
2172      return;
2173    }
2174
2175    LinkedHashSet<ByteString> addedValues = new LinkedHashSet<>();
2176    for (PasswordStorageScheme<?> s : passwordPolicy.getDefaultPasswordStorageSchemes())
2177    {
2178      if (! existingDefaultSchemes.contains(toLowerCase(s.getStorageSchemeName())))
2179      {
2180        try
2181        {
2182          ByteString encodedPassword = encodePassword(password, s);
2183          addedValues.add(encodedPassword);
2184          updatedValues.add(encodedPassword);
2185        }
2186        catch (Exception e)
2187        {
2188          logger.traceException(e);
2189
2190          if (logger.isTraceEnabled())
2191          {
2192            logger.traceException(e, "Unable to encode password for user %s using default scheme %s",
2193                userDNString, s.getStorageSchemeName());
2194          }
2195        }
2196      }
2197    }
2198
2199    if (updatedValues.isEmpty())
2200    {
2201      logger.trace(
2202          "Not updating user entry %s because removing deprecated schemes would leave the user without a password.",
2203          userDNString);
2204      return;
2205    }
2206
2207    Attribute a = newAttribute(type, removedValues);
2208    modifications.add(new Modification(ModificationType.DELETE, a, true));
2209
2210    if (! addedValues.isEmpty())
2211    {
2212      Attribute a2 = newAttribute(type, addedValues);
2213      modifications.add(new Modification(ModificationType.ADD, a2, true));
2214    }
2215
2216    if (logger.isTraceEnabled())
2217    {
2218      logger.trace("Updating user entry %s to replace password values encoded with deprecated schemes " +
2219          "with values encoded with the default schemes.", userDNString);
2220    }
2221  }
2222
2223  private PasswordStorageScheme<?> getPasswordStorageScheme(String schemeName)
2224  {
2225    return passwordPolicy.isAuthPasswordSyntax()
2226        ? DirectoryServer.getAuthPasswordStorageScheme(schemeName)
2227        : DirectoryServer.getPasswordStorageScheme(schemeName);
2228  }
2229
2230  private boolean passwordMatches(ByteString password, String[] pwComponents, PasswordStorageScheme<?> scheme)
2231  {
2232    return passwordPolicy.isAuthPasswordSyntax()
2233        ? scheme.authPasswordMatches(password, pwComponents[1], pwComponents[2])
2234        : scheme.passwordMatches(password, ByteString.valueOfUtf8(pwComponents[1]));
2235  }
2236
2237  private ByteString encodePassword(ByteString password, PasswordStorageScheme<?> s) throws DirectoryException
2238  {
2239    return passwordPolicy.isAuthPasswordSyntax()
2240        ? s.encodeAuthPassword(password)
2241        : s.encodePasswordWithScheme(password);
2242  }
2243
2244  /**
2245   * Indicates whether password history information should be maintained for this user.
2246   *
2247   * @return  {@code true} if password history information should be maintained for this user, or {@code false} if not.
2248   */
2249  public boolean maintainHistory()
2250  {
2251    return passwordPolicy.getPasswordHistoryCount() > 0
2252        || passwordPolicy.getPasswordHistoryDuration() > 0;
2253  }
2254
2255  /**
2256   * Indicates whether the provided password is equal to any of the current passwords,
2257   * or any of the passwords in the history.
2258   *
2259   * @param  password  The password for which to make the determination.
2260   *
2261   * @return  {@code true} if the provided password is equal to any of the current passwords or any of the passwords
2262   *          in the history, or {@code false} if not.
2263   */
2264  public boolean isPasswordInHistory(ByteString password)
2265  {
2266    if (! maintainHistory())
2267    {
2268      if (logger.isTraceEnabled())
2269      {
2270        logger.trace("Returning false because password history checking is disabled.");
2271      }
2272      return false;
2273    }
2274
2275    // Check to see if the provided password is equal to any of the current passwords.
2276    // If so, then we'll consider it to be in the history.
2277    if (passwordMatches(password))
2278    {
2279      if (logger.isTraceEnabled())
2280      {
2281        logger.trace("Returning true because the provided password is currently in use.");
2282      }
2283      return true;
2284    }
2285
2286    // Get the attribute containing the history and check to see if any of the values is equal to the provided password.
2287    // However, first prune the list by size and duration if necessary.
2288    TreeMap<Long, ByteString> historyMap = getSortedHistoryValues(null);
2289
2290    int historyCount = passwordPolicy.getPasswordHistoryCount();
2291    if (historyCount > 0 && historyMap.size() > historyCount)
2292    {
2293      int numToDelete = historyMap.size() - historyCount;
2294      Iterator<Long> iterator = historyMap.keySet().iterator();
2295      while (iterator.hasNext() && numToDelete > 0)
2296      {
2297        iterator.next();
2298        iterator.remove();
2299        numToDelete--;
2300      }
2301    }
2302
2303    long historyDuration = passwordPolicy.getPasswordHistoryDuration();
2304    if (historyDuration > 0L)
2305    {
2306      long retainDate = currentTime - 1000 * historyDuration;
2307      Iterator<Long> iterator = historyMap.keySet().iterator();
2308      while (iterator.hasNext())
2309      {
2310        long historyDate = iterator.next();
2311        if (historyDate >= retainDate)
2312        {
2313          break;
2314        }
2315        iterator.remove();
2316      }
2317    }
2318
2319    for (ByteString v : historyMap.values())
2320    {
2321      if (historyValueMatches(password, v))
2322      {
2323        if (logger.isTraceEnabled())
2324        {
2325          logger.trace("Returning true because the password is in the history.");
2326        }
2327
2328        return true;
2329      }
2330    }
2331
2332    // If we've gotten here, then the password isn't in the history.
2333    if (logger.isTraceEnabled())
2334    {
2335      logger.trace("Returning false because the password isn't in the history.");
2336    }
2337    return false;
2338  }
2339
2340  /**
2341   * Gets a sorted list of the password history values contained in the user's entry.
2342   * The values will be sorted by timestamp.
2343   *
2344   * @param  removeAttrs  A list into which any values will be placed that could not be properly decoded.
2345   *                      It may be {@code null} if this is not needed.
2346   */
2347  private TreeMap<Long,ByteString> getSortedHistoryValues(List<Attribute> removeAttrs)
2348  {
2349    TreeMap<Long, ByteString> historyMap = new TreeMap<>();
2350    AttributeType historyType = DirectoryServer.getSchema().getAttributeType(OP_ATTR_PWPOLICY_HISTORY_LC);
2351    for (Attribute a : userEntry.getAttribute(historyType))
2352    {
2353      for (ByteString v : a)
2354      {
2355        String histStr = v.toString();
2356        int hashPos = histStr.indexOf('#');
2357        if (hashPos <= 0)
2358        {
2359          logger.trace("Found value %s in the history with no timestamp.  Marking it for removal.", histStr);
2360
2361          if (removeAttrs != null)
2362          {
2363            removeAttrs.add(Attributes.create(a.getAttributeDescription().getAttributeType(), v));
2364          }
2365        }
2366        else
2367        {
2368          try
2369          {
2370            ByteString timeValue = ByteString.valueOfUtf8(histStr.substring(0, hashPos));
2371            long timestamp = GeneralizedTimeSyntax.decodeGeneralizedTimeValue(timeValue);
2372            historyMap.put(timestamp, v);
2373          }
2374          catch (Exception e)
2375          {
2376            if (logger.isTraceEnabled())
2377            {
2378              logger.traceException(e);
2379              logger.trace("Could not decode the timestamp in history value %s -- %s.  Marking it for removal.",
2380                  histStr, e.getLocalizedMessage());
2381            }
2382
2383            if (removeAttrs != null)
2384            {
2385              removeAttrs.add(Attributes.create(a.getAttributeDescription().getAttributeType(), v));
2386            }
2387          }
2388        }
2389      }
2390    }
2391
2392    return historyMap;
2393  }
2394
2395  /**
2396   * Indicates whether the provided password matches the given history value.
2397   *
2398   * @param  password      The clear-text password for which to make the determination.
2399   * @param  historyValue  The encoded history value to compare against the clear-text password.
2400   *
2401   * @return  {@code true} if the provided password matches the history value, or {@code false} if not.
2402   */
2403  private boolean historyValueMatches(ByteString password, ByteString historyValue) {
2404    // According to draft-behera-ldap-password-policy, password history values should be in the format
2405    // time#syntaxoid#encodedvalue.  In this method, we only care about the syntax OID and encoded password.
2406    try
2407    {
2408      String histStr  = historyValue.toString();
2409      int    hashPos1 = histStr.indexOf('#');
2410      if (hashPos1 <= 0)
2411      {
2412        if (logger.isTraceEnabled())
2413        {
2414          logger.trace("Returning false because the password history value didn't include any hash characters.");
2415        }
2416
2417        return false;
2418      }
2419
2420      int hashPos2 = histStr.indexOf('#', hashPos1+1);
2421      if (hashPos2 < 0)
2422      {
2423        if (logger.isTraceEnabled())
2424        {
2425          logger.trace("Returning false because the password history value only had one hash character.");
2426        }
2427
2428        return false;
2429      }
2430
2431      String syntaxOID = toLowerCase(histStr.substring(hashPos1+1, hashPos2));
2432      if (SYNTAX_AUTH_PASSWORD_OID.equals(syntaxOID))
2433      {
2434        boolean passwordMatches = encodedAuthPasswordMatches(password, histStr.substring(hashPos2+1));
2435        logResult("auth", passwordMatches);
2436        return passwordMatches;
2437      }
2438      else if (SYNTAX_USER_PASSWORD_OID.equals(syntaxOID) || SYNTAX_OCTET_STRING_OID.equals(syntaxOID))
2439      {
2440        boolean passwordMatches = encodedUserPasswordMatches(password, histStr.substring(hashPos2+1));
2441        logResult("user", passwordMatches);
2442        return passwordMatches;
2443      }
2444      else
2445      {
2446        if (logger.isTraceEnabled())
2447        {
2448          logger.trace("Returning false because the syntax OID " + syntaxOID +
2449              " didn't match for either the auth or user password syntax.");
2450        }
2451
2452        return false;
2453      }
2454    }
2455    catch (Exception e)
2456    {
2457      if (logger.isTraceEnabled())
2458      {
2459        logger.traceException(e);
2460        logger.trace("Returning false because of an exception:  " + stackTraceToSingleLineString(e));
2461      }
2462
2463      return false;
2464    }
2465  }
2466
2467  private boolean encodedAuthPasswordMatches(ByteString password, String encodedAuthPassword) throws DirectoryException
2468  {
2469    String[] authPWComponents = AuthPasswordSyntax.decodeAuthPassword(encodedAuthPassword);
2470    PasswordStorageScheme<?> scheme = DirectoryServer.getAuthPasswordStorageScheme(authPWComponents[0]);
2471    return scheme.authPasswordMatches(password, authPWComponents[1], authPWComponents[2]);
2472  }
2473
2474  private boolean encodedUserPasswordMatches(ByteString password, String encodedUserPassword) throws DirectoryException
2475  {
2476    String[] userPWComponents = UserPasswordSyntax.decodeUserPassword(encodedUserPassword);
2477    PasswordStorageScheme<?> scheme = DirectoryServer.getPasswordStorageScheme(userPWComponents[0]);
2478    return scheme.passwordMatches(password, ByteString.valueOfUtf8(userPWComponents[1]));
2479  }
2480
2481  private void logResult(String passwordType, boolean passwordMatches)
2482  {
2483    if (passwordMatches)
2484    {
2485      logger.trace("Returning true because the %s password history value matched.", passwordType);
2486    }
2487    else
2488    {
2489      logger.trace("Returning false because the %s password history value did not match.", passwordType);
2490    }
2491  }
2492
2493  /**
2494   * Updates the password history information for this user by adding one of the passwords to it.
2495   * It will choose the first password encoded using a secure storage scheme, and will fall back to
2496   * a password encoded using an insecure storage scheme if necessary.
2497   */
2498  public void updatePasswordHistory()
2499  {
2500    for (Attribute a : userEntry.getAttribute(passwordPolicy.getPasswordAttribute()))
2501    {
2502      ByteString insecurePassword = null;
2503      for (ByteString v : a)
2504      {
2505        try
2506        {
2507          PasswordStorageScheme<?> scheme = getPasswordStorageScheme(v);
2508
2509          if (scheme.isStorageSchemeSecure())
2510          {
2511            addPasswordToHistory(v.toString());
2512            insecurePassword = null;
2513            // no need to check any more values for this attribute
2514            break;
2515          }
2516          else if (insecurePassword == null)
2517          {
2518            insecurePassword = v;
2519          }
2520        }
2521        catch (DirectoryException e)
2522        {
2523          if (logger.isTraceEnabled())
2524          {
2525            logger.trace("Encoded password " + v + " cannot be decoded and cannot be added to history.");
2526          }
2527        }
2528      }
2529      // If we get here we haven't found a password encoded securely, so we have to use one of the
2530      // other values.
2531      if (insecurePassword != null)
2532      {
2533        addPasswordToHistory(insecurePassword.toString());
2534      }
2535    }
2536  }
2537
2538  /**
2539   * Adds the provided password to the password history.  If appropriate, one or more old passwords may be
2540   * evicted from the list if the total size would exceed the configured count, or if passwords are older
2541   * than the configured duration.
2542   *
2543   * @param  encodedPassword  The encoded password (in either user password or auth password format)
2544   *                          to be added to the history.
2545   */
2546  private void addPasswordToHistory(String encodedPassword)
2547  {
2548    if (! maintainHistory())
2549    {
2550      if (logger.isTraceEnabled())
2551      {
2552        logger.trace("Not doing anything because password history maintenance is disabled.");
2553      }
2554
2555      return;
2556    }
2557
2558    // Get a sorted list of the existing values to see if there are any that should be removed.
2559    LinkedList<Attribute> removeAttrs = new LinkedList<>();
2560    TreeMap<Long, ByteString> historyMap = getSortedHistoryValues(removeAttrs);
2561
2562    // If there is a maximum number of values to retain and we would be over the limit with the new value,
2563    // then get rid of enough values (oldest first) to satisfy the count.
2564    AttributeType historyType = DirectoryServer.getSchema().getAttributeType(OP_ATTR_PWPOLICY_HISTORY_LC);
2565    int historyCount = passwordPolicy.getPasswordHistoryCount();
2566    if  (historyCount > 0 && historyMap.size() >= historyCount)
2567    {
2568      int numToDelete = historyMap.size() - historyCount + 1;
2569      LinkedHashSet<ByteString> removeValues = new LinkedHashSet<>(numToDelete);
2570      Iterator<ByteString> iterator = historyMap.values().iterator();
2571      while (iterator.hasNext() && numToDelete > 0)
2572      {
2573        ByteString v = iterator.next();
2574        removeValues.add(v);
2575        iterator.remove();
2576        numToDelete--;
2577
2578        if (logger.isTraceEnabled())
2579        {
2580          logger.trace("Removing history value %s to preserve the history count.", v);
2581        }
2582      }
2583
2584      if (! removeValues.isEmpty())
2585      {
2586        removeAttrs.add(newAttribute(historyType, removeValues));
2587      }
2588    }
2589
2590    // If there is a maximum duration, then get rid of any values that would be over the duration.
2591    long historyDuration = passwordPolicy.getPasswordHistoryDuration();
2592    if (historyDuration > 0L)
2593    {
2594      long minAgeToKeep = currentTime - 1000L * historyDuration;
2595      Iterator<Long> iterator = historyMap.keySet().iterator();
2596      LinkedHashSet<ByteString> removeValues = new LinkedHashSet<>();
2597      while (iterator.hasNext())
2598      {
2599        long timestamp = iterator.next();
2600        if (timestamp >= minAgeToKeep)
2601        {
2602          break;
2603        }
2604
2605        ByteString v = historyMap.get(timestamp);
2606        removeValues.add(v);
2607        iterator.remove();
2608
2609        if (logger.isTraceEnabled())
2610        {
2611          logger.trace("Removing history value %s to preserve the history duration.", v);
2612        }
2613      }
2614
2615      if (! removeValues.isEmpty())
2616      {
2617        removeAttrs.add(newAttribute(historyType, removeValues));
2618      }
2619    }
2620
2621    // At this point, we can add the new value.  However, we want to make sure that its timestamp
2622    // (which is the current time) doesn't conflict with any value already in the list.  If there is a conflict,
2623    // then simply add one to it until we don't have any more conflicts.
2624    long newTimestamp = currentTime;
2625    while (historyMap.containsKey(newTimestamp))
2626    {
2627      newTimestamp++;
2628    }
2629    String newHistStr = GeneralizedTimeSyntax.format(newTimestamp) + "#" +
2630        passwordPolicy.getPasswordAttribute().getSyntax().getOID() + "#" + encodedPassword;
2631    Attribute newHistAttr = Attributes.create(historyType, newHistStr);
2632
2633    if (logger.isTraceEnabled())
2634    {
2635      logger.trace("Going to add history value " + newHistStr);
2636    }
2637
2638    // Apply the changes, either by adding modifications or by directly updating the entry.
2639    for (Attribute a : removeAttrs)
2640    {
2641      modifications.add(new Modification(ModificationType.DELETE, a, true));
2642    }
2643
2644    modifications.add(new Modification(ModificationType.ADD, newHistAttr, true));
2645  }
2646
2647  private Attribute newAttribute(AttributeType type, LinkedHashSet<ByteString> values)
2648  {
2649    AttributeBuilder builder = new AttributeBuilder(type);
2650    builder.addAll(values);
2651    return builder.toAttribute();
2652  }
2653
2654  /**
2655   * Retrieves the password history state values for the user.  This is only intended for testing purposes.
2656   *
2657   * @return  The password history state values for the user.
2658   */
2659  public String[] getPasswordHistoryValues()
2660  {
2661    ArrayList<String> historyValues = new ArrayList<>();
2662    AttributeType historyType = DirectoryServer.getSchema().getAttributeType(OP_ATTR_PWPOLICY_HISTORY_LC);
2663    for (Attribute a : userEntry.getAttribute(historyType))
2664    {
2665      for (ByteString v : a)
2666      {
2667        historyValues.add(v.toString());
2668      }
2669    }
2670    return historyValues.toArray(new String[historyValues.size()]);
2671  }
2672
2673  /**
2674   * Clears the password history state information for the user.  This is only intended for testing purposes.
2675   */
2676  public void clearPasswordHistory()
2677  {
2678    logger.trace("Clearing password history for user %s", userDNString);
2679
2680    clearAttribute(OP_ATTR_PWPOLICY_HISTORY_LC);
2681  }
2682
2683  /**
2684   * Generates a new password for the user.
2685   *
2686   * @return  The new password that has been generated, or <CODE>null</CODE> if no password generator has been defined.
2687   *
2688   * @throws  DirectoryException  If an error occurs while attempting to generate the new password.
2689   */
2690  public ByteString generatePassword()
2691      throws DirectoryException
2692  {
2693    PasswordGenerator<?> generator = passwordPolicy.getPasswordGenerator();
2694    if (generator == null)
2695    {
2696      if (logger.isTraceEnabled())
2697      {
2698        logger.trace("Unable to generate a new password for user %s because no password generator has been defined" +
2699            "in the associated password policy.", userDNString);
2700      }
2701
2702      return null;
2703    }
2704
2705    return generator.generatePassword(userEntry);
2706  }
2707
2708  /**
2709   * Generates an account status notification for this user.
2710   *
2711   * @param  notificationType        The type for the account status notification.
2712   * @param  userEntry               The entry for the user to which this notification applies.
2713   * @param  message                 The human-readable message for the notification.
2714   * @param  notificationProperties  The set of properties for the notification.
2715   */
2716  public void generateAccountStatusNotification(
2717      AccountStatusNotificationType notificationType,
2718      Entry userEntry, LocalizableMessage message,
2719      Map<AccountStatusNotificationProperty,List<String>> notificationProperties)
2720  {
2721    generateAccountStatusNotification(
2722        new AccountStatusNotification(notificationType, userEntry, message, notificationProperties));
2723  }
2724
2725  private void generateAccountStatusNotification(AccountStatusNotification notification)
2726  {
2727    Collection<AccountStatusNotificationHandler<?>> handlers = passwordPolicy.getAccountStatusNotificationHandlers();
2728    for (AccountStatusNotificationHandler<?> handler : handlers)
2729    {
2730      handler.handleStatusNotification(notification);
2731    }
2732  }
2733
2734  /**
2735   * Retrieves the set of modifications that correspond to changes made in password policy processing
2736   * that may need to be applied to the user entry.
2737   *
2738   * @return  The set of modifications that correspond to changes made in password policy processing
2739   *          that may need to be applied to the user entry.
2740   */
2741  public List<Modification> getModifications()
2742  {
2743    return modifications;
2744  }
2745
2746  @Override
2747  public void finalizeStateAfterBind()
2748         throws DirectoryException
2749  {
2750    // If there are no modifications, then there's nothing to do.
2751    if (modifications.isEmpty())
2752    {
2753      return;
2754    }
2755
2756    // Convert the set of modifications to a set of LDAP modifications.
2757    ArrayList<RawModification> modList = new ArrayList<>();
2758    for (Modification m : modifications)
2759    {
2760      modList.add(RawModification.create(m.getModificationType(), new LDAPAttribute(m.getAttribute())));
2761    }
2762
2763    InternalClientConnection conn = getRootConnection();
2764    ModifyOperation internalModify = conn.processModify(ByteString.valueOfUtf8(userDNString), modList);
2765
2766    ResultCode resultCode = internalModify.getResultCode();
2767    if (resultCode != ResultCode.SUCCESS)
2768    {
2769      LocalizableMessage message = ERR_PWPSTATE_CANNOT_UPDATE_USER_ENTRY.get(
2770          userDNString, internalModify.getErrorMessage());
2771
2772      // If this is a root user, or if the password policy says that we should ignore these problems,
2773      // then log a warning message.  Otherwise, cause the bind to fail.
2774      if (DirectoryServer.isRootDN(userEntry.getName())
2775          || passwordPolicy.getStateUpdateFailurePolicy() == PasswordPolicyCfgDefn.StateUpdateFailurePolicy.IGNORE)
2776      {
2777        logger.error(message);
2778      }
2779      else
2780      {
2781        throw new DirectoryException(resultCode, message);
2782      }
2783    }
2784  }
2785}