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