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