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
010     * trunk/opends/resource/legal-notices/OpenDS.LICENSE
011     * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
012     * See the License for the specific language governing permissions
013     * and limitations under the License.
014     *
015     * When distributing Covered Code, include this CDDL HEADER in each
016     * file and include the License file at
017     * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
018     * add the following below this CDDL HEADER, with the fields enclosed
019     * by brackets "[]" replaced with your own identifying information:
020     *      Portions Copyright [yyyy] [name of copyright owner]
021     *
022     * CDDL HEADER END
023     *
024     *
025     *      Copyright 2006-2008 Sun Microsystems, Inc.
026     */
027    package org.opends.server.core;
028    
029    
030    
031    import java.text.SimpleDateFormat;
032    import java.util.ArrayList;
033    import java.util.Collection;
034    import java.util.Date;
035    import java.util.HashSet;
036    import java.util.Iterator;
037    import java.util.LinkedHashSet;
038    import java.util.LinkedList;
039    import java.util.List;
040    import java.util.Map;
041    import java.util.Set;
042    import java.util.TreeMap;
043    
044    import org.opends.messages.Message;
045    import org.opends.messages.MessageBuilder;
046    import org.opends.server.admin.std.meta.PasswordPolicyCfgDefn;
047    import org.opends.server.admin.std.server.PasswordValidatorCfg;
048    import org.opends.server.api.AccountStatusNotificationHandler;
049    import org.opends.server.api.PasswordGenerator;
050    import org.opends.server.api.PasswordStorageScheme;
051    import org.opends.server.api.PasswordValidator;
052    import org.opends.server.loggers.ErrorLogger;
053    import org.opends.server.loggers.debug.DebugTracer;
054    import org.opends.server.protocols.asn1.ASN1OctetString;
055    import org.opends.server.protocols.internal.InternalClientConnection;
056    import org.opends.server.protocols.ldap.LDAPAttribute;
057    import org.opends.server.schema.AuthPasswordSyntax;
058    import org.opends.server.schema.GeneralizedTimeSyntax;
059    import org.opends.server.schema.UserPasswordSyntax;
060    import org.opends.server.types.AccountStatusNotification;
061    import org.opends.server.types.AccountStatusNotificationProperty;
062    import org.opends.server.types.AccountStatusNotificationType;
063    import org.opends.server.types.Attribute;
064    import org.opends.server.types.AttributeType;
065    import org.opends.server.types.AttributeValue;
066    import org.opends.server.types.ByteString;
067    import org.opends.server.types.ConditionResult;
068    import org.opends.server.types.DebugLogLevel;
069    import org.opends.server.types.DirectoryException;
070    import org.opends.server.types.DN;
071    import org.opends.server.types.Entry;
072    import org.opends.server.types.Modification;
073    import org.opends.server.types.ModificationType;
074    import org.opends.server.types.Operation;
075    import org.opends.server.types.RawModification;
076    import org.opends.server.types.ResultCode;
077    import org.opends.server.util.TimeThread;
078    
079    import static org.opends.server.config.ConfigConstants.*;
080    import static org.opends.server.loggers.debug.DebugLogger.*;
081    import static org.opends.messages.CoreMessages.*;
082    import static org.opends.server.schema.SchemaConstants.*;
083    import static org.opends.server.util.StaticUtils.*;
084    
085    
086    
087    /**
088     * This class provides a data structure for holding password policy state
089     * information for a user account.
090     */
091    public class PasswordPolicyState
092    {
093      /**
094       * The tracer object for the debug logger.
095       */
096      private static final DebugTracer TRACER = getTracer();
097    
098    
099    
100      // The user entry with which this state information is associated.
101      private final Entry userEntry;
102    
103      // Indicates whether the user entry itself should be updated or if the updates
104      // should be stored as modifications.
105      private final boolean updateEntry;
106    
107      // The string representation of the user's DN.
108      private final String userDNString;
109    
110      // The password policy with which the account is associated.
111      private final PasswordPolicy passwordPolicy;
112    
113      // The current time for use in all password policy calculations.
114      private final long currentTime;
115    
116      // The time that the user's password was last changed.
117      private long passwordChangedTime = Long.MIN_VALUE;
118    
119      // Indicates whether the user's account is expired.
120      private ConditionResult isAccountExpired = ConditionResult.UNDEFINED;
121    
122      // Indicates whether the user's account is disabled.
123      private ConditionResult isDisabled = ConditionResult.UNDEFINED;
124    
125      // Indicates whether the user's password is expired.
126      private ConditionResult isPasswordExpired = ConditionResult.UNDEFINED;
127    
128      // Indicates whether the warning to send to the client would be the first
129      // warning for the user.
130      private ConditionResult isFirstWarning = ConditionResult.UNDEFINED;
131    
132      // Indicates whether the user's account is locked by the idle lockout.
133      private ConditionResult isIdleLocked = ConditionResult.UNDEFINED;
134    
135      // Indicates whether the user may use a grace login if the password is expired
136      // and there are one or more grace logins remaining.
137      private ConditionResult mayUseGraceLogin = ConditionResult.UNDEFINED;
138    
139      // Indicates whether the user's password must be changed.
140      private ConditionResult mustChangePassword = ConditionResult.UNDEFINED;
141    
142      // Indicates whether the user should be warned of an upcoming expiration.
143      private ConditionResult shouldWarn = ConditionResult.UNDEFINED;
144    
145      // The number of seconds until the user's account is automatically unlocked.
146      private int secondsUntilUnlock = Integer.MIN_VALUE;
147    
148      // The set of authentication failure times for this user.
149      private List<Long> authFailureTimes = null;
150    
151      // The set of grace login times for this user.
152      private List<Long> graceLoginTimes = null;
153    
154      // The time that the user's account should expire (or did expire).
155      private long accountExpirationTime = Long.MIN_VALUE;
156    
157      // The time that the user's entry was locked due to too many authentication
158      // failures.
159      private long failureLockedTime = Long.MIN_VALUE;
160    
161      // The time that the user last authenticated to the Directory Server.
162      private long lastLoginTime = Long.MIN_VALUE;
163    
164      // The time that the user's password should expire (or did expire).
165      private long passwordExpirationTime = Long.MIN_VALUE;
166    
167      // The last required change time with which the user complied.
168      private long requiredChangeTime = Long.MIN_VALUE;
169    
170      // The time that the user was first warned about an upcoming expiration.
171      private long warnedTime = Long.MIN_VALUE;
172    
173      // The set of modifications that should be applied to the user's entry.
174      private LinkedList<Modification> modifications
175           = new LinkedList<Modification>();
176    
177    
178    
179      /**
180       * Creates a new password policy state object with the provided information.
181       *
182       * @param  userEntry    The entry with the user account.
183       * @param  updateEntry  Indicates whether changes should update the provided
184       *                      user entry directly or whether they should be
185       *                      collected as a set of modifications.
186       *
187       * @throws  DirectoryException  If a problem occurs while attempting to
188       *                              determine the password policy for the user or
189       *                              perform any other state initialization.
190       */
191      public PasswordPolicyState(Entry userEntry, boolean updateEntry)
192           throws DirectoryException
193      {
194        this(userEntry, updateEntry, TimeThread.getTime(), false);
195      }
196    
197    
198    
199      /**
200       * Creates a new password policy state object with the provided information.
201       * Note that this version of the constructor should only be used for testing
202       * purposes when the tests should be evaluated with a fixed time rather than
203       * the actual current time.  For all other purposes, the other constructor
204       * should be used.
205       *
206       * @param  userEntry          The entry with the user account.
207       * @param  updateEntry        Indicates whether changes should update the
208       *                            provided user entry directly or whether they
209       *                            should be collected as a set of modifications.
210       * @param  currentTime        The time to use as the current time for all
211       *                            time-related determinations.
212       * @param  useDefaultOnError  Indicates whether the server should fall back to
213       *                            using the default password policy if there is a
214       *                            problem with the configured policy for the user.
215       *
216       * @throws  DirectoryException  If a problem occurs while attempting to
217       *                              determine the password policy for the user or
218       *                              perform any other state initialization.
219       */
220      public PasswordPolicyState(Entry userEntry, boolean updateEntry,
221                                 long currentTime, boolean useDefaultOnError)
222           throws DirectoryException
223      {
224        this.userEntry   = userEntry;
225        this.updateEntry = updateEntry;
226        this.currentTime = currentTime;
227    
228        userDNString     = userEntry.getDN().toString();
229        passwordPolicy   = getPasswordPolicyInternal(this.userEntry,
230                                                     useDefaultOnError);
231    
232        // Get the password changed time for the user.
233        AttributeType type
234             = DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_CHANGED_TIME_LC);
235        if (type == null)
236        {
237          type = DirectoryServer.getDefaultAttributeType(
238               OP_ATTR_PWPOLICY_CHANGED_TIME);
239        }
240    
241        passwordChangedTime = getGeneralizedTime(type);
242        if (passwordChangedTime <= 0)
243        {
244          // Get the time that the user's account was created.
245          AttributeType createTimeType
246               = DirectoryServer.getAttributeType(OP_ATTR_CREATE_TIMESTAMP_LC);
247          if (createTimeType == null)
248          {
249            createTimeType
250                = DirectoryServer.getDefaultAttributeType(OP_ATTR_CREATE_TIMESTAMP);
251          }
252          passwordChangedTime = getGeneralizedTime(createTimeType);
253    
254          if (passwordChangedTime <= 0)
255          {
256            passwordChangedTime = 0;
257    
258            if (debugEnabled())
259            {
260              TRACER.debugWarning("Could not determine password changed time for " +
261                  "user %s.", userDNString);
262            }
263          }
264        }
265      }
266    
267    
268    
269      /**
270       * Retrieves the password policy for the user. If the user entry contains the
271       * ds-pwp-password-policy-dn attribute (whether real or virtual), that
272       * password policy is returned, otherwise the default password policy is
273       * returned.
274       *
275       * @param  userEntry          The user entry.
276       * @param  useDefaultOnError  Indicates whether the server should fall back to
277       *                            using the default password policy if there is a
278       *                            problem with the configured policy for the user.
279       *
280       * @return  The password policy for the user.
281       *
282       * @throws  DirectoryException  If a problem occurs while attempting to
283       *                              determine the password policy for the user.
284       */
285      private static PasswordPolicy getPasswordPolicyInternal(Entry userEntry,
286                                         boolean useDefaultOnError)
287           throws DirectoryException
288      {
289        String userDNString = userEntry.getDN().toString();
290        AttributeType type =
291             DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_POLICY_DN, true);
292    
293        List<Attribute> attrList = userEntry.getAttribute(type);
294        if (attrList != null)
295        {
296          for (Attribute a : attrList)
297          {
298            if(a.getValues().isEmpty()) continue;
299    
300            AttributeValue v = a.getValues().iterator().next();
301            DN subentryDN;
302            try
303            {
304              subentryDN = DN.decode(v.getValue());
305            }
306            catch (Exception e)
307            {
308              if (debugEnabled())
309              {
310                TRACER.debugCaught(DebugLogLevel.ERROR, e);
311              }
312    
313              if (debugEnabled())
314              {
315                TRACER.debugError("Could not parse password policy subentry " +
316                    "DN %s for user %s: %s",
317                           v.getStringValue(), userDNString,
318                           stackTraceToSingleLineString(e));
319              }
320    
321              Message message = ERR_PWPSTATE_CANNOT_DECODE_SUBENTRY_VALUE_AS_DN.get(
322                  v.getStringValue(), userDNString, e.getMessage());
323              if (useDefaultOnError)
324              {
325                ErrorLogger.logError(message);
326                return DirectoryServer.getDefaultPasswordPolicy();
327              }
328              else
329              {
330                throw new DirectoryException(ResultCode.INVALID_DN_SYNTAX, message,
331                                             e);
332              }
333            }
334    
335            PasswordPolicy policy = DirectoryServer.getPasswordPolicy(subentryDN);
336            if (policy == null)
337            {
338              if (debugEnabled())
339              {
340                TRACER.debugError("Password policy subentry %s for user %s " +
341                     "is not defined in the Directory Server.",
342                           String.valueOf(subentryDN), userDNString);
343              }
344    
345              Message message = ERR_PWPSTATE_NO_SUCH_POLICY.get(
346                  userDNString, String.valueOf(subentryDN));
347              if (useDefaultOnError)
348              {
349                ErrorLogger.logError(message);
350                return DirectoryServer.getDefaultPasswordPolicy();
351              }
352              else
353              {
354                throw new DirectoryException(
355                     DirectoryServer.getServerErrorResultCode(), message);
356              }
357            }
358    
359            if (debugEnabled())
360            {
361              TRACER.debugInfo("Using password policy subentry %s for user %s.",
362                  String.valueOf(subentryDN), userDNString);
363            }
364    
365            return policy;
366          }
367        }
368    
369        // There is no policy subentry defined: use the default.
370        if (debugEnabled())
371        {
372          TRACER.debugInfo("Using the default password policy for user %s",
373              userDNString);
374        }
375    
376        return DirectoryServer.getDefaultPasswordPolicy();
377      }
378    
379    
380    
381       /**
382        * Retrieves the value of the specified attribute as a string.
383        *
384        * @param  attributeType  The attribute type whose value should be retrieved.
385        *
386        * @return  The value of the specified attribute as a string, or
387        *          <CODE>null</CODE> if there is no such value.
388        */
389      private String getValue(AttributeType attributeType)
390      {
391        String stringValue = null;
392    
393        List<Attribute> attrList = userEntry.getAttribute(attributeType);
394        if (attrList != null)
395        {
396          for (Attribute a : attrList)
397          {
398            if (a.getValues().isEmpty()) continue;
399    
400            stringValue = a.getValues().iterator().next().getStringValue();
401            break ;
402          }
403        }
404    
405        if (stringValue == null)
406        {
407          if (debugEnabled())
408          {
409            TRACER.debugInfo("Returning null because attribute %s does not " +
410                "exist in user entry %s",
411                attributeType.getNameOrOID(), userDNString);
412          }
413        }
414        else
415        {
416          if (debugEnabled())
417          {
418            TRACER.debugInfo("Returning value %s for user %s",
419                stringValue, userDNString);
420          }
421        }
422    
423        return stringValue;
424      }
425    
426    
427    
428      /**
429       * Retrieves the value of the specified attribute from the user's entry as a
430       * time in generalized time format.
431       *
432       * @param  attributeType  The attribute type whose value should be parsed as a
433       *                        generalized time value.
434       *
435       * @return  The requested time, or -1 if it could not be determined.
436       *
437       * @throws  DirectoryException  If a problem occurs while attempting to
438       *                              decode the value as a generalized time.
439       */
440      private long getGeneralizedTime(AttributeType attributeType)
441              throws DirectoryException
442      {
443        long timeValue = -1 ;
444    
445        List<Attribute> attrList = userEntry.getAttribute(attributeType);
446        if (attrList != null)
447        {
448          for (Attribute a : attrList)
449          {
450            if (a.getValues().isEmpty()) continue;
451    
452            AttributeValue v = a.getValues().iterator().next();
453            try
454            {
455              timeValue = GeneralizedTimeSyntax.decodeGeneralizedTimeValue(
456                              v.getNormalizedValue());
457            }
458            catch (Exception e)
459            {
460              if (debugEnabled())
461              {
462                TRACER.debugCaught(DebugLogLevel.ERROR, e);
463    
464                TRACER.debugWarning("Unable to decode value %s for attribute %s " +
465                    "in user entry %s: %s",
466                    v.getStringValue(), attributeType.getNameOrOID(),
467                    userDNString, stackTraceToSingleLineString(e));
468              }
469    
470              Message message = ERR_PWPSTATE_CANNOT_DECODE_GENERALIZED_TIME.
471                  get(v.getStringValue(), attributeType.getNameOrOID(),
472                      userDNString, String.valueOf(e));
473              throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
474                                           message, e);
475            }
476            break ;
477          }
478        }
479    
480        if (timeValue == -1)
481        {
482          if (debugEnabled())
483          {
484            TRACER.debugInfo("Returning -1 because attribute %s does not " +
485                "exist in user entry %s",
486                attributeType.getNameOrOID(), userDNString);
487          }
488        }
489        // FIXME: else to be consistent...
490    
491        return timeValue;
492      }
493    
494    
495    
496      /**
497       * Retrieves the set of values of the specified attribute from the user's
498       * entry in generalized time format.
499       *
500       * @param  attributeType  The attribute type whose values should be parsed as
501       *                        generalized time values.
502       *
503       * @return  The set of generalized time values, or an empty list if there are
504       *          none.
505       *
506       * @throws  DirectoryException  If a problem occurs while attempting to
507       *                              decode a value as a generalized time.
508       */
509      private List<Long> getGeneralizedTimes(AttributeType attributeType)
510              throws DirectoryException
511      {
512        ArrayList<Long> timeValues = new ArrayList<Long>();
513    
514        List<Attribute> attrList = userEntry.getAttribute(attributeType);
515        if (attrList != null)
516        {
517          for (Attribute a : attrList)
518          {
519            for (AttributeValue v : a.getValues())
520            {
521              try
522              {
523                timeValues.add(GeneralizedTimeSyntax.decodeGeneralizedTimeValue(
524                                                           v.getNormalizedValue()));
525              }
526              catch (Exception e)
527              {
528                if (debugEnabled())
529                {
530                  TRACER.debugCaught(DebugLogLevel.ERROR, e);
531    
532                  TRACER.debugWarning("Unable to decode value %s for attribute %s" +
533                      "in user entry %s: %s",
534                      v.getStringValue(), attributeType.getNameOrOID(),
535                      userDNString, stackTraceToSingleLineString(e));
536                }
537    
538                Message message = ERR_PWPSTATE_CANNOT_DECODE_GENERALIZED_TIME.
539                    get(v.getStringValue(), attributeType.getNameOrOID(),
540                        userDNString, String.valueOf(e));
541                throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
542                                             message, e);
543              }
544            }
545          }
546        }
547    
548        if (timeValues.isEmpty())
549        {
550          if (debugEnabled())
551          {
552            TRACER.debugInfo("Returning an empty list because attribute %s " +
553                "does not exist in user entry %s",
554                attributeType.getNameOrOID(), userDNString);
555          }
556        }
557        return timeValues;
558      }
559    
560    
561    
562      /**
563       * Retrieves the value of the specified attribute from the user's entry as a
564       * Boolean.
565       *
566       * @param  attributeType  The attribute type whose value should be parsed as a
567       *                        Boolean.
568       *
569       * @return  The attribute's value represented as a ConditionResult value, or
570       *          ConditionResult.UNDEFINED if the specified attribute does not
571       *          exist in the entry.
572       *
573       * @throws  DirectoryException  If the value cannot be decoded as a Boolean.
574       */
575      private ConditionResult getBoolean(AttributeType attributeType)
576              throws DirectoryException
577      {
578        List<Attribute> attrList = userEntry.getAttribute(attributeType);
579        if (attrList != null)
580        {
581          for (Attribute a : attrList)
582          {
583            if (a.getValues().isEmpty()) continue;
584    
585            String valueString
586                 = toLowerCase(a.getValues().iterator().next().getStringValue());
587    
588            if (valueString.equals("true") || valueString.equals("yes") ||
589                valueString.equals("on") || valueString.equals("1"))
590            {
591              if (debugEnabled())
592              {
593                TRACER.debugInfo("Attribute %s resolves to true for user entry " +
594                    "%s", attributeType.getNameOrOID(), userDNString);
595              }
596    
597              return ConditionResult.TRUE;
598            }
599    
600            if (valueString.equals("false") || valueString.equals("no") ||
601                     valueString.equals("off") || valueString.equals("0"))
602            {
603              if (debugEnabled())
604              {
605                TRACER.debugInfo("Attribute %s resolves to false for user " +
606                    "entry %s", attributeType.getNameOrOID(), userDNString);
607              }
608    
609              return ConditionResult.FALSE;
610            }
611    
612            if(debugEnabled())
613            {
614              TRACER.debugError("Unable to resolve value %s for attribute %s " +
615                  "in user entry %s as a Boolean.",
616                  valueString, attributeType.getNameOrOID(),
617                  userDNString);
618            }
619    
620            Message message = ERR_PWPSTATE_CANNOT_DECODE_BOOLEAN.get(
621                valueString, attributeType.getNameOrOID(), userDNString);
622            throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
623                message);
624          }
625        }
626    
627        if (debugEnabled())
628        {
629          TRACER.debugInfo("Returning %s because attribute %s does not exist " +
630              "in user entry %s",
631              ConditionResult.UNDEFINED.toString(),
632              attributeType.getNameOrOID(), userDNString);
633        }
634    
635        return ConditionResult.UNDEFINED;
636      }
637    
638    
639    
640      /**
641       * Retrieves the password policy associated with this state information.
642       *
643       * @return  The password policy associated with this state information.
644       */
645      public PasswordPolicy getPolicy()
646      {
647        return passwordPolicy;
648      }
649    
650    
651    
652      /**
653       * Retrieves the time that the password was last changed.
654       *
655       * @return  The time that the password was last changed.
656       */
657      public long getPasswordChangedTime()
658      {
659        return passwordChangedTime;
660      }
661    
662    
663    
664      /**
665       * Retrieves the time that this password policy state object was created.
666       *
667       * @return  The time that this password policy state object was created.
668       */
669      public long getCurrentTime()
670      {
671        return currentTime;
672      }
673    
674    
675    
676      /**
677       * Retrieves the set of values for the password attribute from the user entry.
678       *
679       * @return  The set of values for the password attribute from the user entry.
680       */
681      public LinkedHashSet<AttributeValue> getPasswordValues()
682      {
683        List<Attribute> attrList =
684             userEntry.getAttribute(passwordPolicy.getPasswordAttribute());
685        if (attrList != null)
686        {
687          for (Attribute a : attrList)
688          {
689            if (a.getValues().isEmpty()) continue;
690    
691            return a.getValues();
692          }
693        }
694    
695        return new LinkedHashSet<AttributeValue>(0);
696      }
697    
698    
699    
700      /**
701       * Sets a new value for the password changed time equal to the current time.
702       */
703      public void setPasswordChangedTime()
704      {
705        setPasswordChangedTime(currentTime);
706      }
707    
708    
709    
710      /**
711       * Sets a new value for the password changed time equal to the specified time.
712       * This method should generally only be used for testing purposes, since the
713       * variant that uses the current time is preferred almost everywhere else.
714       *
715       * @param  passwordChangedTime  The time to use
716       */
717      public void setPasswordChangedTime(long passwordChangedTime)
718      {
719        if (debugEnabled())
720        {
721          TRACER.debugInfo("Setting password changed time for user %s to " +
722              "current time of %d", userDNString, currentTime);
723        }
724    
725        // passwordChangedTime is computed in the constructor from values in the
726        // entry.
727        if (this.passwordChangedTime != passwordChangedTime)
728        {
729          this.passwordChangedTime = passwordChangedTime;
730    
731          AttributeType type =
732               DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_CHANGED_TIME_LC);
733          if (type == null)
734          {
735            type = DirectoryServer.getDefaultAttributeType(
736                                        OP_ATTR_PWPOLICY_CHANGED_TIME);
737          }
738    
739          LinkedHashSet<AttributeValue> values =
740               new LinkedHashSet<AttributeValue>(1);
741          String timeValue = GeneralizedTimeSyntax.format(passwordChangedTime);
742          values.add(new AttributeValue(type, timeValue));
743    
744          Attribute a = new Attribute(type, OP_ATTR_PWPOLICY_CHANGED_TIME, values);
745    
746          if (updateEntry)
747          {
748            ArrayList<Attribute> attrList = new ArrayList<Attribute>(1);
749            attrList.add(a);
750            userEntry.putAttribute(type, attrList);
751          }
752          else
753          {
754            modifications.add(new Modification(ModificationType.REPLACE, a, true));
755          }
756        }
757      }
758    
759    
760    
761      /**
762       * Removes the password changed time value from the user's entry.  This should
763       * only be used for testing purposes, as it can really mess things up if you
764       * don't know what you're doing.
765       */
766      public void clearPasswordChangedTime()
767      {
768        if (debugEnabled())
769        {
770          TRACER.debugInfo("Clearing password changed time for user %s",
771              userDNString);
772        }
773    
774        AttributeType type =
775             DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_CHANGED_TIME_LC,
776                                           true);
777        if (updateEntry)
778        {
779          userEntry.removeAttribute(type);
780        }
781        else
782        {
783          Attribute a = new Attribute(type);
784          modifications.add(new Modification(ModificationType.REPLACE, a, true));
785        }
786    
787    
788        // Fall back to using the entry creation time as the password changed time,
789        // if it's defined.  Otherwise, use a value of zero.
790        AttributeType createTimeType =
791             DirectoryServer.getAttributeType(OP_ATTR_CREATE_TIMESTAMP_LC, true);
792        try
793        {
794          passwordChangedTime = getGeneralizedTime(createTimeType);
795          if (passwordChangedTime <= 0)
796          {
797            passwordChangedTime = 0;
798          }
799        }
800        catch (Exception e)
801        {
802          passwordChangedTime = 0;
803        }
804      }
805    
806    
807    
808    
809      /**
810       * Indicates whether the user account has been administratively disabled.
811       *
812       * @return  <CODE>true</CODE> if the user account has been administratively
813       *          disabled, or <CODE>false</CODE> otherwise.
814       */
815      public boolean isDisabled()
816      {
817        if (isDisabled != ConditionResult.UNDEFINED)
818        {
819          if (debugEnabled())
820          {
821            TRACER.debugInfo("Returning stored result of %b for user %s",
822                (isDisabled == ConditionResult.TRUE), userDNString);
823          }
824    
825          return isDisabled == ConditionResult.TRUE;
826        }
827    
828        AttributeType type =
829             DirectoryServer.getAttributeType(OP_ATTR_ACCOUNT_DISABLED, true);
830        try
831        {
832          isDisabled = getBoolean(type);
833        }
834        catch (Exception e)
835        {
836          if (debugEnabled())
837          {
838            TRACER.debugCaught(DebugLogLevel.ERROR, e);
839          }
840    
841          isDisabled = ConditionResult.TRUE;
842          if (debugEnabled())
843          {
844              TRACER.debugWarning("User %s is considered administratively " +
845                  "disabled because an error occurred while attempting to make " +
846                  "the determination: %s.",
847                           userDNString, stackTraceToSingleLineString(e));
848          }
849    
850          return true;
851        }
852    
853        if (isDisabled == ConditionResult.UNDEFINED)
854        {
855          isDisabled = ConditionResult.FALSE;
856          if (debugEnabled())
857          {
858            TRACER.debugInfo("User %s is not administratively disabled since " +
859                "the attribute \"%s\" is not present in the entry.",
860                userDNString, OP_ATTR_ACCOUNT_DISABLED);
861          }
862          return false;
863        }
864    
865        if (debugEnabled())
866        {
867          TRACER.debugInfo("User %s %s administratively disabled.",
868              userDNString,
869              ((isDisabled == ConditionResult.TRUE) ? " is" : " is not"));
870        }
871    
872        return isDisabled == ConditionResult.TRUE;
873      }
874    
875    
876    
877      /**
878       * Updates the user entry to indicate whether user account has been
879       * administratively disabled.
880       *
881       * @param  isDisabled  Indicates whether the user account has been
882       *                     administratively disabled.
883       */
884      public void setDisabled(boolean isDisabled)
885      {
886        if (debugEnabled())
887        {
888          TRACER.debugInfo("Updating user %s to set the disabled flag to %b",
889              userDNString, isDisabled);
890        }
891    
892    
893        if (isDisabled == isDisabled())
894        {
895          return; // requested state matches current state
896        }
897    
898        this.isDisabled = ConditionResult.inverseOf(this.isDisabled);
899    
900        AttributeType type =
901             DirectoryServer.getAttributeType(OP_ATTR_ACCOUNT_DISABLED, true);
902    
903        if (isDisabled)
904        {
905          LinkedHashSet<AttributeValue> values
906               = new LinkedHashSet<AttributeValue>(1);
907          values.add(new AttributeValue(type, String.valueOf(true)));
908          Attribute a = new Attribute(type, OP_ATTR_ACCOUNT_DISABLED, values);
909    
910          if (updateEntry)
911          {
912            ArrayList<Attribute> attrList = new ArrayList<Attribute>(1);
913            attrList.add(a);
914            userEntry.putAttribute(type, attrList);
915          }
916          else
917          {
918            modifications.add(new Modification(ModificationType.REPLACE, a, true));
919          }
920        }
921        else
922        {
923          // erase
924          if (updateEntry)
925          {
926            userEntry.removeAttribute(type);
927          }
928          else
929          {
930            modifications.add(new Modification(ModificationType.REPLACE,
931                                               new Attribute(type), true));
932          }
933        }
934      }
935    
936    
937    
938      /**
939       * Indicates whether the user's account is currently expired.
940       *
941       * @return  <CODE>true</CODE> if the user's account is expired, or
942       *          <CODE>false</CODE> if not.
943       */
944      public boolean isAccountExpired()
945      {
946        if (isAccountExpired != ConditionResult.UNDEFINED)
947        {
948          if (debugEnabled())
949          {
950            TRACER.debugInfo("Returning stored result of %b for user %s",
951                (isAccountExpired == ConditionResult.TRUE), userDNString);
952          }
953    
954          return isAccountExpired == ConditionResult.TRUE;
955        }
956    
957        AttributeType type =
958             DirectoryServer.getAttributeType(OP_ATTR_ACCOUNT_EXPIRATION_TIME,
959                                              true);
960    
961        try
962        {
963          accountExpirationTime = getGeneralizedTime(type);
964         }
965        catch (Exception e)
966        {
967          if (debugEnabled())
968          {
969            TRACER.debugCaught(DebugLogLevel.ERROR, e);
970          }
971    
972          isAccountExpired = ConditionResult.TRUE;
973          if (debugEnabled())
974          {
975              TRACER.debugWarning("User %s is considered to have an expired " +
976                   "account because an error occurred while attempting to make " +
977                   "the determination: %s.",
978                  userDNString, stackTraceToSingleLineString(e));
979          }
980    
981          return true;
982        }
983    
984        if (accountExpirationTime > currentTime)
985        {
986          // The user does have an expiration time, but it hasn't arrived yet.
987          isAccountExpired = ConditionResult.FALSE;
988          if (debugEnabled())
989          {
990            TRACER.debugInfo("The account for user %s is not expired because " +
991                "the expiration time has not yet arrived.", userDNString);
992          }
993        }
994        else if (accountExpirationTime >= 0)
995        {
996          // The user does have an expiration time, and it is in the past.
997          isAccountExpired = ConditionResult.TRUE;
998          if (debugEnabled())
999          {
1000            TRACER.debugInfo("The account for user %s is expired because the " +
1001                "expiration time in that account has passed.", userDNString);
1002          }
1003        }
1004        else
1005        {
1006          // The user doesn't have an expiration time in their entry, so it
1007          // can't be expired.
1008          isAccountExpired = ConditionResult.FALSE;
1009          if (debugEnabled())
1010          {
1011            TRACER.debugInfo("The account for user %s is not expired because " +
1012                "there is no expiration time in the user's entry.",
1013                userDNString);
1014          }
1015        }
1016    
1017        return isAccountExpired == ConditionResult.TRUE;
1018      }
1019    
1020    
1021    
1022      /**
1023       * Retrieves the time at which the user's account will expire.
1024       *
1025       * @return  The time at which the user's account will expire, or -1 if it is
1026       *          not configured with an expiration time.
1027       */
1028      public long getAccountExpirationTime()
1029      {
1030        if (accountExpirationTime == Long.MIN_VALUE)
1031        {
1032          isAccountExpired();
1033        }
1034    
1035        return accountExpirationTime;
1036      }
1037    
1038    
1039    
1040      /**
1041       * Sets the user's account expiration time to the specified value.
1042       *
1043       * @param  accountExpirationTime  The time that the user's account should
1044       *                                expire.
1045       */
1046      public void setAccountExpirationTime(long accountExpirationTime)
1047      {
1048        if (accountExpirationTime < 0)
1049        {
1050          clearAccountExpirationTime();
1051        }
1052        else
1053        {
1054          String timeStr = GeneralizedTimeSyntax.format(accountExpirationTime);
1055    
1056          if (debugEnabled())
1057          {
1058            TRACER.debugInfo("Setting account expiration time for user %s to %s",
1059                userDNString, timeStr);
1060          }
1061    
1062          this.accountExpirationTime = accountExpirationTime;
1063          AttributeType type =
1064               DirectoryServer.getAttributeType(OP_ATTR_ACCOUNT_EXPIRATION_TIME,
1065                                                true);
1066    
1067          LinkedHashSet<AttributeValue> values =
1068               new LinkedHashSet<AttributeValue>(1);
1069          values.add(new AttributeValue(type, timeStr));
1070    
1071          Attribute a = new Attribute(type, OP_ATTR_ACCOUNT_EXPIRATION_TIME,
1072                                      values);
1073    
1074          if (updateEntry)
1075          {
1076            ArrayList<Attribute> attrList = new ArrayList<Attribute>(1);
1077            attrList.add(a);
1078            userEntry.putAttribute(type, attrList);
1079          }
1080          else
1081          {
1082            modifications.add(new Modification(ModificationType.REPLACE, a, true));
1083          }
1084        }
1085      }
1086    
1087    
1088    
1089      /**
1090       * Clears the user's account expiration time.
1091       */
1092      public void clearAccountExpirationTime()
1093      {
1094        if (debugEnabled())
1095        {
1096          TRACER.debugInfo("Clearing account expiration time for user %s",
1097              userDNString);
1098        }
1099    
1100        accountExpirationTime = -1;
1101    
1102        AttributeType type =
1103             DirectoryServer.getAttributeType(OP_ATTR_ACCOUNT_EXPIRATION_TIME,
1104                                              true);
1105    
1106        if (updateEntry)
1107        {
1108          userEntry.removeAttribute(type);
1109        }
1110        else
1111        {
1112          modifications.add(new Modification(ModificationType.REPLACE,
1113                                             new Attribute(type), true));
1114        }
1115      }
1116    
1117    
1118    
1119      /**
1120       * Retrieves the set of times of failed authentication attempts for the user.
1121       * If authentication failure time expiration is enabled, and there are expired
1122       * times in the entry, these times are removed from the instance field and an
1123       * update is provided to delete those values from the entry.
1124       *
1125       * @return  The set of times of failed authentication attempts for the user,
1126       *          which will be an empty list in the case of no valid (unexpired)
1127       *          times in the entry.
1128       */
1129      public List<Long> getAuthFailureTimes()
1130      {
1131        if (authFailureTimes != null)
1132        {
1133          if (debugEnabled())
1134          {
1135            TRACER.debugInfo("Returning stored auth failure time list of %d " +
1136                "elements for user %s" +
1137                authFailureTimes.size(), userDNString);
1138          }
1139    
1140          return authFailureTimes;
1141        }
1142    
1143        AttributeType type =
1144             DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_FAILURE_TIME_LC);
1145        if (type == null)
1146        {
1147          type = DirectoryServer.getDefaultAttributeType(
1148               OP_ATTR_PWPOLICY_FAILURE_TIME);
1149        }
1150    
1151        try
1152        {
1153          authFailureTimes = getGeneralizedTimes(type);
1154        }
1155        catch (Exception e)
1156        {
1157          if (debugEnabled())
1158          {
1159            TRACER.debugCaught(DebugLogLevel.ERROR, e);
1160          }
1161    
1162          if (debugEnabled())
1163          {
1164            TRACER.debugWarning("Error while processing auth failure times " +
1165                 "for user %s: %s",
1166                         userDNString, stackTraceToSingleLineString(e));
1167          }
1168    
1169          authFailureTimes = new ArrayList<Long>();
1170    
1171          if (updateEntry)
1172          {
1173            userEntry.removeAttribute(type);
1174          }
1175          else
1176          {
1177            modifications.add(new Modification(ModificationType.REPLACE,
1178                                               new Attribute(type), true));
1179          }
1180    
1181          return authFailureTimes;
1182        }
1183    
1184        if (authFailureTimes.isEmpty())
1185        {
1186          if (debugEnabled())
1187          {
1188            TRACER.debugInfo("Returning an empty auth failure time list for " +
1189                "user %s because the attribute is absent from the entry.",
1190                userDNString);
1191          }
1192    
1193          return authFailureTimes;
1194        }
1195    
1196        // Remove any expired failures from the list.
1197        if (passwordPolicy.getLockoutFailureExpirationInterval() > 0)
1198        {
1199          LinkedHashSet<AttributeValue> valuesToRemove = null;
1200    
1201          long expirationTime = currentTime -
1202               (passwordPolicy.getLockoutFailureExpirationInterval() * 1000L);
1203          Iterator<Long> iterator = authFailureTimes.iterator();
1204          while (iterator.hasNext())
1205          {
1206            long l = iterator.next();
1207            if (l < expirationTime)
1208            {
1209              if (debugEnabled())
1210              {
1211                TRACER.debugInfo("Removing expired auth failure time %d for " +
1212                    "user %s", l, userDNString);
1213              }
1214    
1215              iterator.remove();
1216    
1217              if (valuesToRemove == null)
1218              {
1219                valuesToRemove = new LinkedHashSet<AttributeValue>();
1220              }
1221    
1222              valuesToRemove.add(new AttributeValue(type,
1223                                                  GeneralizedTimeSyntax.format(l)));
1224            }
1225          }
1226    
1227          if (valuesToRemove != null)
1228          {
1229            if (updateEntry)
1230            {
1231              if (authFailureTimes.isEmpty())
1232              {
1233                userEntry.removeAttribute(type);
1234              }
1235              else
1236              {
1237                LinkedHashSet<AttributeValue> keepValues =
1238                     new LinkedHashSet<AttributeValue>(authFailureTimes.size());
1239                for (Long l : authFailureTimes)
1240                {
1241                  keepValues.add(
1242                       new AttributeValue(type, GeneralizedTimeSyntax.format(l)));
1243                }
1244                ArrayList<Attribute> keepList = new ArrayList<Attribute>(1);
1245                keepList.add(new Attribute(type, OP_ATTR_PWPOLICY_FAILURE_TIME,
1246                                           keepValues));
1247                userEntry.putAttribute(type, keepList);
1248              }
1249            }
1250            else
1251            {
1252              Attribute a = new Attribute(type, OP_ATTR_PWPOLICY_FAILURE_TIME,
1253                                          valuesToRemove);
1254              modifications.add(new Modification(ModificationType.DELETE, a,
1255                                                 true));
1256            }
1257          }
1258        }
1259    
1260        if (debugEnabled())
1261        {
1262          TRACER.debugInfo("Returning auth failure time list of %d elements " +
1263              "for user %s", authFailureTimes.size(), userDNString);
1264        }
1265    
1266        return authFailureTimes;
1267      }
1268    
1269    
1270    
1271      /**
1272       * Updates the set of authentication failure times to include the current
1273       * time. If the number of failures reaches the policy configuration limit,
1274       * lock the account.
1275       */
1276      public void updateAuthFailureTimes()
1277      {
1278        if (passwordPolicy.getLockoutFailureCount() <= 0)
1279        {
1280          return;
1281        }
1282    
1283        if (debugEnabled())
1284        {
1285          TRACER.debugInfo("Updating authentication failure times for user %s",
1286              userDNString);
1287        }
1288    
1289    
1290        List<Long> failureTimes = getAuthFailureTimes();
1291        // Note: failureTimes == this.authFailureTimes
1292        long highestFailureTime = -1;
1293        for (Long l : failureTimes)
1294        {
1295          highestFailureTime = Math.max(l, highestFailureTime);
1296        }
1297    
1298        if (highestFailureTime >= currentTime)
1299        {
1300          highestFailureTime++;
1301        }
1302        else
1303        {
1304          highestFailureTime = currentTime;
1305        }
1306        failureTimes.add(highestFailureTime);
1307    
1308        AttributeType type =
1309             DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_FAILURE_TIME_LC);
1310        if (type == null)
1311        {
1312          type = DirectoryServer.getDefaultAttributeType(
1313                                      OP_ATTR_PWPOLICY_FAILURE_TIME);
1314        }
1315    
1316        LinkedHashSet<AttributeValue> values =
1317               new LinkedHashSet<AttributeValue>(failureTimes.size());
1318        for (Long l : failureTimes)
1319        {
1320          values.add(new AttributeValue(type, GeneralizedTimeSyntax.format(l)));
1321        }
1322    
1323        Attribute a = new Attribute(type, OP_ATTR_PWPOLICY_FAILURE_TIME, values);
1324        ArrayList<Attribute> attrList = new ArrayList<Attribute>(1);
1325        attrList.add(a);
1326    
1327        LinkedHashSet<AttributeValue> addValues =
1328             new LinkedHashSet<AttributeValue>(1);
1329        addValues.add(new AttributeValue(type,
1330                               GeneralizedTimeSyntax.format(highestFailureTime)));
1331        Attribute addAttr = new Attribute(type, OP_ATTR_PWPOLICY_FAILURE_TIME,
1332                                          addValues);
1333    
1334        if (updateEntry)
1335        {
1336          userEntry.putAttribute(type, attrList);
1337        }
1338        else
1339        {
1340          modifications.add(new Modification(ModificationType.ADD, addAttr, true));
1341        }
1342    
1343        // Now check to see if there have been sufficient failures to lock the
1344        // account.
1345        int lockoutCount = passwordPolicy.getLockoutFailureCount();
1346        if ((lockoutCount > 0) && (lockoutCount <= authFailureTimes.size()))
1347        {
1348          setFailureLockedTime(highestFailureTime);
1349          if (debugEnabled())
1350          {
1351            TRACER.debugInfo("Locking user account %s due to too many failures.",
1352                userDNString);
1353          }
1354        }
1355      }
1356    
1357    
1358    
1359      /**
1360       * Explicitly specifies the auth failure times for the associated user.  This
1361       * should generally only be used for testing purposes.  Note that it will also
1362       * set or clear the locked time as appropriate.
1363       *
1364       * @param  authFailureTimes  The set of auth failure times to use for the
1365       *                           account.  An empty list or {@code null} will
1366       *                           clear the account of any existing failures.
1367       */
1368      public void setAuthFailureTimes(List<Long> authFailureTimes)
1369      {
1370        if ((authFailureTimes == null) || authFailureTimes.isEmpty())
1371        {
1372          clearAuthFailureTimes();
1373          clearFailureLockedTime();
1374          return;
1375        }
1376    
1377        long highestFailureTime = -1;
1378        for (Long l : authFailureTimes)
1379        {
1380          highestFailureTime = Math.max(l, highestFailureTime);
1381        }
1382    
1383        AttributeType type =
1384             DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_FAILURE_TIME_LC,
1385                                              true);
1386    
1387        LinkedHashSet<AttributeValue> values =
1388               new LinkedHashSet<AttributeValue>(authFailureTimes.size());
1389        for (Long l : authFailureTimes)
1390        {
1391          values.add(new AttributeValue(type, GeneralizedTimeSyntax.format(l)));
1392        }
1393    
1394        Attribute a = new Attribute(type, OP_ATTR_PWPOLICY_FAILURE_TIME, values);
1395    
1396        if (updateEntry)
1397        {
1398          ArrayList<Attribute> attrList = new ArrayList<Attribute>(1);
1399          attrList.add(a);
1400          userEntry.putAttribute(type, attrList);
1401        }
1402        else
1403        {
1404          modifications.add(new Modification(ModificationType.REPLACE, a, true));
1405        }
1406    
1407        // Now check to see if there have been sufficient failures to lock the
1408        // account.
1409        int lockoutCount = passwordPolicy.getLockoutFailureCount();
1410        if ((lockoutCount > 0) && (lockoutCount <= authFailureTimes.size()))
1411        {
1412          setFailureLockedTime(highestFailureTime);
1413          if (debugEnabled())
1414          {
1415            TRACER.debugInfo("Locking user account %s due to too many failures.",
1416                userDNString);
1417          }
1418        }
1419      }
1420    
1421    
1422    
1423      /**
1424       * Updates the user entry to remove any record of previous authentication
1425       * failure times.
1426       */
1427      private void clearAuthFailureTimes()
1428      {
1429        if (debugEnabled())
1430        {
1431          TRACER.debugInfo("Clearing authentication failure times for user %s",
1432              userDNString);
1433        }
1434    
1435        List<Long> failureTimes = getAuthFailureTimes();
1436        if (failureTimes.isEmpty())
1437        {
1438          return;
1439        }
1440    
1441        failureTimes.clear(); // Note: failureTimes == this.authFailureTimes
1442    
1443        AttributeType type =
1444             DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_FAILURE_TIME_LC);
1445        if (type == null)
1446        {
1447          type = DirectoryServer.getDefaultAttributeType(
1448                                      OP_ATTR_PWPOLICY_FAILURE_TIME);
1449        }
1450    
1451        if (updateEntry)
1452        {
1453          userEntry.removeAttribute(type);
1454        }
1455        else
1456        {
1457          modifications.add(new Modification(ModificationType.REPLACE,
1458                                             new Attribute(type), true));
1459        }
1460      }
1461    
1462    
1463      /**
1464       * Retrieves the time of an authentication failure lockout for the user.
1465       *
1466       * @return  The time of an authentication failure lockout for the user, or -1
1467       *          if no such time is present in the entry.
1468       */
1469      private long getFailureLockedTime()
1470      {
1471        if (failureLockedTime != Long.MIN_VALUE)
1472        {
1473          return failureLockedTime;
1474        }
1475    
1476        AttributeType type =
1477             DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_LOCKED_TIME_LC);
1478        if (type == null)
1479        {
1480          type = DirectoryServer.getDefaultAttributeType(
1481               OP_ATTR_PWPOLICY_LOCKED_TIME);
1482        }
1483    
1484        try
1485        {
1486          failureLockedTime = getGeneralizedTime(type);
1487        }
1488        catch (Exception e)
1489        {
1490          if (debugEnabled())
1491          {
1492            TRACER.debugCaught(DebugLogLevel.ERROR, e);
1493          }
1494    
1495          failureLockedTime = currentTime;
1496          if (debugEnabled())
1497          {
1498            TRACER.debugWarning("Returning current time for user %s because an " +
1499                "error occurred: %s",
1500                         userDNString, stackTraceToSingleLineString(e));
1501          }
1502    
1503          return failureLockedTime;
1504        }
1505    
1506        // An expired locked time is handled in lockedDueToFailures.
1507        return failureLockedTime;
1508      }
1509    
1510    
1511    
1512      /**
1513        Sets the failure lockout attribute in the entry to the requested time.
1514    
1515        @param time  The time to which to set the entry's failure lockout attribute.
1516       */
1517      private void setFailureLockedTime(final long time)
1518      {
1519        if (time == getFailureLockedTime())
1520        {
1521          return;
1522        }
1523    
1524        failureLockedTime = time;
1525    
1526        AttributeType type =
1527             DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_LOCKED_TIME_LC);
1528        if (type == null)
1529        {
1530          type = DirectoryServer.getDefaultAttributeType(
1531                                      OP_ATTR_PWPOLICY_LOCKED_TIME);
1532        }
1533    
1534        LinkedHashSet<AttributeValue> values = new LinkedHashSet<AttributeValue>(1);
1535        values.add(new AttributeValue(type,
1536                            GeneralizedTimeSyntax.format(failureLockedTime)));
1537        Attribute a = new Attribute(type, OP_ATTR_PWPOLICY_LOCKED_TIME, values);
1538    
1539        if (updateEntry)
1540        {
1541          ArrayList<Attribute> attrList = new ArrayList<Attribute>(1);
1542          attrList.add(a);
1543          userEntry.putAttribute(type, attrList);
1544        }
1545        else
1546        {
1547          modifications.add(new Modification(ModificationType.REPLACE, a, true));
1548        }
1549      }
1550    
1551    
1552    
1553      /**
1554       * Updates the user entry to remove any record of previous authentication
1555       * failure lockout.
1556       */
1557      private void clearFailureLockedTime()
1558      {
1559        if (debugEnabled())
1560        {
1561          TRACER.debugInfo("Clearing failure lockout time for user %s.",
1562              userDNString);
1563        }
1564    
1565        if (-1L == getFailureLockedTime())
1566        {
1567          return;
1568        }
1569    
1570        failureLockedTime = -1L;
1571    
1572        AttributeType type =
1573             DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_LOCKED_TIME_LC);
1574        if (type == null)
1575        {
1576          type = DirectoryServer.getDefaultAttributeType(
1577                                      OP_ATTR_PWPOLICY_LOCKED_TIME);
1578        }
1579    
1580        if (updateEntry)
1581        {
1582          userEntry.removeAttribute(type);
1583        }
1584        else
1585        {
1586          modifications.add(new Modification(ModificationType.REPLACE,
1587                                             new Attribute(type), true));
1588        }
1589      }
1590    
1591    
1592    
1593      /**
1594       * Indicates whether the associated user should be considered locked out as a
1595       * result of too many authentication failures. In the case of an expired
1596       * lock-out, this routine produces the update to clear the lock-out attribute
1597       * and the authentication failure timestamps.
1598       * In case the failure lockout time is absent from the entry, but sufficient
1599       * authentication failure timestamps are present in the entry, this routine
1600       * produces the update to set the lock-out attribute.
1601       *
1602       * @return  <CODE>true</CODE> if the user is currently locked out due to too
1603       *          many authentication failures, or <CODE>false</CODE> if not.
1604       */
1605      public boolean lockedDueToFailures()
1606      {
1607        // FIXME: Introduce a state field to cache the computed value of this
1608        // method. Note that only a cached "locked" status can be returned due to
1609        // the possibility of intervening updates to this.failureLockedTime by
1610        // updateAuthFailureTimes.
1611    
1612        // Check if the feature is enabled in the policy.
1613        final int maxFailures = passwordPolicy.getLockoutFailureCount();
1614        if (maxFailures <= 0)
1615        {
1616          if (debugEnabled())
1617          {
1618            TRACER.debugInfo("Returning false for user %s because lockout due " +
1619                "to failures is not enabled.", userDNString);
1620          }
1621    
1622          return false;
1623        }
1624    
1625        // Get the locked time from the user's entry. If it is present and not
1626        // expired, the account is locked. If it is absent, the failure timestamps
1627        // must be checked, since failure timestamps sufficient to lock the
1628        // account could be produced across the synchronization topology within the
1629        // synchronization latency. Also, note that IETF
1630        // draft-behera-ldap-password-policy-09 specifies "19700101000000Z" as
1631        // the value to be set under a "locked until reset" regime; however, this
1632        // implementation accepts the value as a locked entry, but observes the
1633        // lockout expiration policy for all values including this one.
1634        // FIXME: This "getter" is unusual in that it might produce an update to the
1635        // entry in two cases. Does it make sense to factor the methods so that,
1636        // e.g., an expired lockout is reported, and clearing the lockout is left to
1637        // the caller?
1638        if (getFailureLockedTime() < 0L)
1639        {
1640          // There was no locked time present in the entry; however, sufficient
1641          // failure times might have accumulated to trigger a lockout.
1642          if (getAuthFailureTimes().size() < maxFailures)
1643          {
1644            if (debugEnabled())
1645            {
1646              TRACER.debugInfo("Returning false for user %s because there is " +
1647                  "no locked time.", userDNString);
1648            }
1649    
1650            return false;
1651          }
1652    
1653          // The account isn't locked but should be, so do so now.
1654          setFailureLockedTime(currentTime);// FIXME: set to max(failureTimes)?
1655    
1656          if (debugEnabled())
1657          {
1658            TRACER.debugInfo("Locking user %s because there were enough " +
1659                "existing failures even though there was no account locked time.",
1660                userDNString);
1661          }
1662          // Fall through...
1663        }
1664    
1665        // There is a failure locked time, but it may be expired.
1666        if (passwordPolicy.getLockoutDuration() > 0)
1667        {
1668          final long unlockTime = getFailureLockedTime() +
1669               (1000L * passwordPolicy.getLockoutDuration());
1670          if (unlockTime > currentTime)
1671          {
1672            secondsUntilUnlock = (int) ((unlockTime - currentTime) / 1000);
1673    
1674            if (debugEnabled())
1675            {
1676              TRACER.debugInfo("Returning true for user %s because there is a " +
1677                  "locked time and the lockout duration has not been reached.",
1678                  userDNString);
1679            }
1680    
1681            return true;
1682          }
1683    
1684          // The lockout in the entry has expired...
1685          clearFailureLockout();
1686    
1687          if (debugEnabled())
1688          {
1689            TRACER.debugInfo("Returning false for user %s " +
1690                "because the existing lockout has expired.", userDNString);
1691          }
1692    
1693          assert -1L == getFailureLockedTime();
1694          return false;
1695        }
1696    
1697        if (debugEnabled())
1698        {
1699          TRACER.debugInfo("Returning true for user %s " +
1700              "because there is a locked time and no lockout duration.",
1701              userDNString);
1702        }
1703    
1704        assert -1L <= getFailureLockedTime();
1705        return true;
1706      }
1707    
1708    
1709    
1710      /**
1711       * Retrieves the length of time in seconds until the user's account is
1712       * automatically unlocked.  This should only be called after calling
1713       * <CODE>lockedDueToFailures</CODE>.
1714       *
1715       * @return  The length of time in seconds until the user's account is
1716       *          automatically unlocked, or -1 if the account is not locked or the
1717       *          lockout requires administrative action to clear.
1718       */
1719      public int getSecondsUntilUnlock()
1720      {
1721        // secondsUntilUnlock is only set when failureLockedTime is present and
1722        // PasswordPolicy.getLockoutDuration is enabled; hence it is not
1723        // unreasonable to find secondsUntilUnlock uninitialized.
1724        assert failureLockedTime != Long.MIN_VALUE;
1725    
1726        return (secondsUntilUnlock < 0) ? -1 : secondsUntilUnlock;
1727      }
1728    
1729    
1730    
1731      /**
1732       * Updates the user account to remove any record of a previous lockout due to
1733       * failed authentications.
1734       */
1735      public void clearFailureLockout()
1736      {
1737        clearAuthFailureTimes();
1738        clearFailureLockedTime();
1739      }
1740    
1741    
1742    
1743      /**
1744       * Retrieves the time that the user last authenticated to the Directory
1745       * Server.
1746       *
1747       * @return  The time that the user last authenticated to the Directory Server,
1748       *          or -1 if it cannot be determined.
1749       */
1750      public long getLastLoginTime()
1751      {
1752        if (lastLoginTime != Long.MIN_VALUE)
1753        {
1754          if (debugEnabled())
1755          {
1756            TRACER.debugInfo("Returning stored last login time of %d for " +
1757                "user %s.", lastLoginTime, userDNString);
1758          }
1759    
1760          return lastLoginTime;
1761        }
1762    
1763        // The policy configuration must be checked since the entry cannot be
1764        // evaluated without both an attribute name and timestamp format.
1765        AttributeType type   = passwordPolicy.getLastLoginTimeAttribute();
1766        String        format = passwordPolicy.getLastLoginTimeFormat();
1767    
1768        if ((type == null) || (format == null))
1769        {
1770          lastLoginTime = -1;
1771          if (debugEnabled())
1772          {
1773            TRACER.debugInfo("Returning -1 for user %s because no last login " +
1774                "time will be maintained.", userDNString);
1775          }
1776    
1777          return lastLoginTime;
1778        }
1779    
1780        lastLoginTime = -1;
1781        List<Attribute> attrList = userEntry.getAttribute(type);
1782    
1783        if (attrList != null)
1784        {
1785          for (Attribute a : attrList)
1786          {
1787            if (a.getValues().isEmpty()) continue;
1788    
1789            String valueString = a.getValues().iterator().next().getStringValue();
1790    
1791            try
1792            {
1793              SimpleDateFormat dateFormat = new SimpleDateFormat(format);
1794              lastLoginTime = dateFormat.parse(valueString).getTime();
1795    
1796              if (debugEnabled())
1797              {
1798                TRACER.debugInfo("Returning last login time of %d for user %s" +
1799                    "decoded using current last login time format.",
1800                    lastLoginTime, userDNString);
1801              }
1802    
1803              return lastLoginTime;
1804            }
1805            catch (Exception e)
1806            {
1807              if (debugEnabled())
1808              {
1809                TRACER.debugCaught(DebugLogLevel.ERROR, e);
1810              }
1811    
1812              // This could mean that the last login time was encoded using a
1813              // previous format.
1814              for (String f : passwordPolicy.getPreviousLastLoginTimeFormats())
1815              {
1816                try
1817                {
1818                  SimpleDateFormat dateFormat = new SimpleDateFormat(f);
1819                  lastLoginTime = dateFormat.parse(valueString).getTime();
1820    
1821                  if (debugEnabled())
1822                  {
1823                    TRACER.debugInfo("Returning last login time of %d for " +
1824                        "user %s decoded using previous last login time format " +
1825                        "of %s.", lastLoginTime, userDNString, f);
1826                  }
1827    
1828                  return lastLoginTime;
1829                }
1830                catch (Exception e2)
1831                {
1832                  if (debugEnabled())
1833                  {
1834                    TRACER.debugCaught(DebugLogLevel.ERROR, e);
1835                  }
1836                }
1837              }
1838    
1839              assert lastLoginTime == -1;
1840              if (debugEnabled())
1841              {
1842                  TRACER.debugWarning("Returning -1 for user %s because the " +
1843                      "last login time value %s could not be parsed using any " +
1844                      "known format.", userDNString, valueString);
1845              }
1846    
1847              return lastLoginTime;
1848            }
1849          }
1850        }
1851    
1852        assert lastLoginTime == -1;
1853        if (debugEnabled())
1854        {
1855          TRACER.debugInfo("Returning %d for user %s because no last " +
1856              "login time value exists.", lastLoginTime, userDNString);
1857        }
1858    
1859        return lastLoginTime;
1860      }
1861    
1862    
1863    
1864      /**
1865       * Updates the user entry to set the current time as the last login time.
1866       */
1867      public void setLastLoginTime()
1868      {
1869        setLastLoginTime(currentTime);
1870      }
1871    
1872    
1873    
1874      /**
1875       * Updates the user entry to use the specified last login time.  This should
1876       * be used primarily for testing purposes, as the variant that uses the
1877       * current time should be used most of the time.
1878       *
1879       * @param  lastLoginTime  The last login time to set in the user entry.
1880       */
1881      public void setLastLoginTime(long lastLoginTime)
1882      {
1883        AttributeType type = passwordPolicy.getLastLoginTimeAttribute();
1884        String format = passwordPolicy.getLastLoginTimeFormat();
1885    
1886        if ((type == null) || (format == null))
1887        {
1888          return;
1889        }
1890    
1891        String timestamp;
1892        try
1893        {
1894          SimpleDateFormat dateFormat = new SimpleDateFormat(format);
1895          timestamp = dateFormat.format(new Date(lastLoginTime));
1896          this.lastLoginTime = dateFormat.parse(timestamp).getTime();
1897        }
1898        catch (Exception e)
1899        {
1900          if (debugEnabled())
1901          {
1902            TRACER.debugCaught(DebugLogLevel.ERROR, e);
1903          }
1904    
1905          if (debugEnabled())
1906          {
1907            TRACER.debugWarning("Unable to set last login time for user %s " +
1908                "because an error occurred: %s",
1909                         userDNString, stackTraceToSingleLineString(e));
1910          }
1911    
1912          return;
1913        }
1914    
1915    
1916        String existingTimestamp = getValue(type);
1917        if ((existingTimestamp != null) && timestamp.equals(existingTimestamp))
1918        {
1919          if (debugEnabled())
1920          {
1921            TRACER.debugInfo("Not updating last login time for user %s " +
1922                "because the new value matches the existing value.",
1923                userDNString);
1924          }
1925    
1926          return;
1927        }
1928    
1929    
1930        LinkedHashSet<AttributeValue> values = new LinkedHashSet<AttributeValue>(1);
1931        values.add(new AttributeValue(type, timestamp));
1932    
1933        Attribute a = new Attribute(type, type.getNameOrOID(), values);
1934    
1935        if (updateEntry)
1936        {
1937          ArrayList<Attribute> attrList = new ArrayList<Attribute>(1);
1938          attrList.add(a);
1939          userEntry.putAttribute(type, attrList);
1940        }
1941        else
1942        {
1943          modifications.add(new Modification(ModificationType.REPLACE, a, true));
1944        }
1945    
1946        if (debugEnabled())
1947        {
1948          TRACER.debugInfo("Updated the last login time for user %s to %s",
1949              userDNString, timestamp);
1950        }
1951      }
1952    
1953    
1954    
1955      /**
1956       * Clears the last login time from the user's entry.  This should generally be
1957       * used only for testing purposes.
1958       */
1959      public void clearLastLoginTime()
1960      {
1961        if (debugEnabled())
1962        {
1963          TRACER.debugInfo("Clearing last login time for user %s", userDNString);
1964        }
1965    
1966        lastLoginTime = -1;
1967    
1968        AttributeType type =
1969             DirectoryServer.getAttributeType(OP_ATTR_LAST_LOGIN_TIME, true);
1970    
1971        if (updateEntry)
1972        {
1973          userEntry.removeAttribute(type);
1974        }
1975        else
1976        {
1977          modifications.add(new Modification(ModificationType.REPLACE,
1978                                             new Attribute(type), true));
1979        }
1980      }
1981    
1982    
1983    
1984      /**
1985       * Indicates whether the user's account is currently locked because it has
1986       * been idle for too long.
1987       *
1988       * @return  <CODE>true</CODE> if the user's account is locked because it has
1989       *          been idle for too long, or <CODE>false</CODE> if not.
1990       */
1991      public boolean lockedDueToIdleInterval()
1992      {
1993        if (isIdleLocked != ConditionResult.UNDEFINED)
1994        {
1995          if (debugEnabled())
1996          {
1997            TRACER.debugInfo("Returning stored result of %b for user %s",
1998                (isIdleLocked == ConditionResult.TRUE), userDNString);
1999          }
2000    
2001          return isIdleLocked == ConditionResult.TRUE;
2002        }
2003    
2004        // Return immediately if this feature is disabled, since the feature is not
2005        // responsible for any state attribute in the entry.
2006        if (passwordPolicy.getIdleLockoutInterval() <= 0)
2007        {
2008          isIdleLocked = ConditionResult.FALSE;
2009    
2010          if (debugEnabled())
2011          {
2012            TRACER.debugInfo("Returning false for user %s because no idle " +
2013                "lockout interval is defined.", userDNString);
2014          }
2015          return false;
2016        }
2017    
2018        long lockTime = currentTime -
2019                             (1000L * passwordPolicy.getIdleLockoutInterval());
2020        if(lockTime < 0) lockTime = 0;
2021    
2022        long lastLoginTime = getLastLoginTime();
2023        if (lastLoginTime > lockTime || passwordChangedTime > lockTime)
2024        {
2025          isIdleLocked = ConditionResult.FALSE;
2026          if (debugEnabled())
2027          {
2028            StringBuilder reason = new StringBuilder();
2029            if(lastLoginTime > lockTime)
2030            {
2031              reason.append("the last login time is in an acceptable window");
2032            }
2033            else
2034            {
2035              if(lastLoginTime < 0)
2036              {
2037                reason.append("there is no last login time, but ");
2038              }
2039              reason.append(
2040                  "the password changed time is in an acceptable window");
2041            }
2042            TRACER.debugInfo("Returning false for user %s because %s.",
2043                userDNString, reason.toString());
2044          }
2045        }
2046        else
2047        {
2048          isIdleLocked = ConditionResult.TRUE;
2049          if (debugEnabled())
2050          {
2051            String reason = (lastLoginTime < 0)
2052                ? "there is no last login time and the password " +
2053                "changed time is not in an acceptable window"
2054                : "neither last login time nor password " +
2055                "changed time are in an acceptable window";
2056            TRACER.debugInfo("Returning true for user %s because %s.",
2057                userDNString, reason);
2058          }
2059        }
2060    
2061        return isIdleLocked == ConditionResult.TRUE;
2062      }
2063    
2064    
2065    
2066    /**
2067    * Indicates whether the user's password must be changed before any other
2068    * operation can be performed.
2069    *
2070    * @return  <CODE>true</CODE> if the user's password must be changed before
2071    *          any other operation can be performed.
2072    */
2073      public boolean mustChangePassword()
2074      {
2075        if(mustChangePassword != ConditionResult.UNDEFINED)
2076        {
2077          if (debugEnabled())
2078          {
2079            TRACER.debugInfo("Returning stored result of %b for user %s.",
2080                (mustChangePassword == ConditionResult.TRUE), userDNString);
2081          }
2082    
2083          return mustChangePassword == ConditionResult.TRUE;
2084        }
2085    
2086        // If the password policy doesn't use force change on add or force change on
2087        // reset, or if it forbids the user from changing his password, then return
2088        // false.
2089        // FIXME: the only getter responsible for a state attribute (pwdReset) that
2090        // considers the policy before checking the entry for the presence of the
2091        // attribute.
2092        if (! (passwordPolicy.allowUserPasswordChanges()
2093               && (passwordPolicy.forceChangeOnAdd()
2094                   || passwordPolicy.forceChangeOnReset())))
2095        {
2096          mustChangePassword = ConditionResult.FALSE;
2097          if (debugEnabled())
2098          {
2099            TRACER.debugInfo("Returning false for user %s because neither " +
2100                "force change on add nor force change on reset is enabled, " +
2101                "or users are not allowed to self-modify passwords.",
2102                userDNString);
2103    
2104          }
2105    
2106          return false;
2107        }
2108    
2109        AttributeType type =
2110               DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_RESET_REQUIRED_LC);
2111        if (type == null)
2112        {
2113          type = DirectoryServer.getDefaultAttributeType(
2114               OP_ATTR_PWPOLICY_RESET_REQUIRED);
2115        }
2116    
2117        try
2118        {
2119          mustChangePassword = getBoolean(type);
2120        }
2121        catch (Exception e)
2122        {
2123          if (debugEnabled())
2124          {
2125            TRACER.debugCaught(DebugLogLevel.ERROR, e);
2126    
2127            TRACER.debugWarning("Returning true for user %s because an error " +
2128                "occurred: %s", userDNString, stackTraceToSingleLineString(e));
2129          }
2130    
2131          mustChangePassword = ConditionResult.TRUE;
2132    
2133          return true;
2134        }
2135    
2136        if(mustChangePassword == ConditionResult.UNDEFINED)
2137        {
2138          mustChangePassword = ConditionResult.FALSE;
2139          if (debugEnabled())
2140          {
2141            TRACER.debugInfo("Returning %b for user since the attribute \"%s\"" +
2142                " is not present in the entry.",
2143                false, userDNString, OP_ATTR_PWPOLICY_RESET_REQUIRED);
2144          }
2145    
2146          return false;
2147        }
2148    
2149        if (debugEnabled())
2150        {
2151          TRACER.debugInfo("Returning %b for user %s.",
2152              (mustChangePassword == ConditionResult.TRUE), userDNString);
2153        }
2154    
2155        return mustChangePassword == ConditionResult.TRUE;
2156      }
2157    
2158    
2159    
2160    /**
2161    * Updates the user entry to indicate whether the user's password must be
2162    * changed.
2163    *
2164    * @param  mustChangePassword  Indicates whether the user's password must be
2165    *                             changed.
2166    */
2167      public void setMustChangePassword(boolean mustChangePassword)
2168      {
2169        if (debugEnabled())
2170        {
2171          TRACER.debugInfo("Updating user %s to set the reset flag to %b",
2172              userDNString, mustChangePassword);
2173        }
2174    
2175        if (mustChangePassword == mustChangePassword())
2176        {
2177          return;  // requested state matches current state
2178        }
2179    
2180        this.mustChangePassword =
2181                ConditionResult.inverseOf(this.mustChangePassword);
2182    
2183        AttributeType type =
2184             DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_RESET_REQUIRED_LC);
2185        if (type == null)
2186        {
2187          type = DirectoryServer.getDefaultAttributeType(
2188                                      OP_ATTR_PWPOLICY_RESET_REQUIRED);
2189        }
2190    
2191        if (mustChangePassword)
2192        {
2193          LinkedHashSet<AttributeValue> values =
2194               new LinkedHashSet<AttributeValue>(1);
2195          values.add(new AttributeValue(type, String.valueOf(true)));
2196          Attribute a = new Attribute(type, OP_ATTR_PWPOLICY_RESET_REQUIRED,
2197                                      values);
2198    
2199          if (updateEntry)
2200          {
2201            ArrayList<Attribute> attrList = new ArrayList<Attribute>(1);
2202            attrList.add(a);
2203            userEntry.putAttribute(type, attrList);
2204          }
2205          else
2206          {
2207            modifications.add(new Modification(ModificationType.REPLACE, a, true));
2208          }
2209        }
2210        else
2211        {
2212          // erase
2213          if (updateEntry)
2214          {
2215            userEntry.removeAttribute(type);
2216          }
2217          else
2218          {
2219            modifications.add(new Modification(ModificationType.REPLACE,
2220                                               new Attribute(type), true));
2221          }
2222        }
2223      }
2224    
2225    
2226    
2227      /**
2228       * Indicates whether the user's account is locked because the password has
2229       * been reset by an administrator but the user did not change the password in
2230       * a timely manner.
2231       *
2232       * @return  <CODE>true</CODE> if the user's account is locked because of the
2233       *          maximum reset age, or <CODE>false</CODE> if not.
2234       */
2235      public boolean lockedDueToMaximumResetAge()
2236      {
2237        // This feature is reponsible for neither a state field nor an entry state
2238        // attribute.
2239        if (passwordPolicy.getMaximumPasswordResetAge() <= 0)
2240        {
2241          if (debugEnabled())
2242          {
2243            TRACER.debugInfo("Returning false for user %s because there is no " +
2244                "maximum reset age.", userDNString);
2245          }
2246    
2247          return false;
2248        }
2249    
2250        if (! mustChangePassword())
2251        {
2252          if (debugEnabled())
2253          {
2254            TRACER.debugInfo("Returning false for user %s because the user's " +
2255                "password has not been reset.", userDNString);
2256          }
2257    
2258          return false;
2259        }
2260    
2261        long maxResetTime = passwordChangedTime +
2262            (1000L * passwordPolicy.getMaximumPasswordResetAge());
2263        boolean locked = (maxResetTime < currentTime);
2264    
2265        if (debugEnabled())
2266        {
2267          TRACER.debugInfo("Returning %b for user %s after comparing the " +
2268              "current and max reset times.", locked, userDNString);
2269        }
2270    
2271        return locked;
2272      }
2273    
2274    
2275    
2276      /**
2277       * Retrieves the time that the user's password should expire (if the
2278       * expiration is in the future) or did expire (if the expiration was in the
2279       * past).  Note that this method should be called after the
2280       * <CODE>lockedDueToMaximumResetAge</CODE> method because grace logins will
2281       * not be allowed in the case that the maximum reset age has passed whereas
2282       * they may be used for expiration due to maximum password age or forced
2283       * change time.
2284       *
2285       * @return  The time that the user's password should/did expire, or -1 if it
2286       *          should not expire.
2287       */
2288      public long getPasswordExpirationTime()
2289      {
2290        if (passwordExpirationTime == Long.MIN_VALUE)
2291        {
2292          passwordExpirationTime = Long.MAX_VALUE;
2293    
2294          boolean checkWarning = false;
2295    
2296          int maxAge = passwordPolicy.getMaximumPasswordAge();
2297          if (maxAge > 0)
2298          {
2299            long expTime = passwordChangedTime + (1000L*maxAge);
2300            if (expTime < passwordExpirationTime)
2301            {
2302              passwordExpirationTime = expTime;
2303              checkWarning   = true;
2304            }
2305          }
2306    
2307          int maxResetAge = passwordPolicy.getMaximumPasswordResetAge();
2308          if (mustChangePassword() && (maxResetAge > 0))
2309          {
2310            long expTime = passwordChangedTime + (1000L*maxResetAge);
2311            if (expTime < passwordExpirationTime)
2312            {
2313              passwordExpirationTime = expTime;
2314              checkWarning   = false;
2315            }
2316          }
2317    
2318          long mustChangeTime = passwordPolicy.getRequireChangeByTime();
2319          if (mustChangeTime > 0)
2320          {
2321            long reqChangeTime = getRequiredChangeTime();
2322            if ((reqChangeTime != mustChangeTime) &&
2323                (mustChangeTime < passwordExpirationTime))
2324            {
2325              passwordExpirationTime = mustChangeTime;
2326              checkWarning   = true;
2327            }
2328          }
2329    
2330          if (passwordExpirationTime == Long.MAX_VALUE)
2331          {
2332            passwordExpirationTime = -1;
2333            shouldWarn             = ConditionResult.FALSE;
2334            isFirstWarning         = ConditionResult.FALSE;
2335            isPasswordExpired      = ConditionResult.FALSE;
2336            mayUseGraceLogin       = ConditionResult.TRUE;
2337          }
2338          else if (checkWarning)
2339          {
2340            mayUseGraceLogin = ConditionResult.TRUE;
2341    
2342            int warningInterval = passwordPolicy.getWarningInterval();
2343            if (warningInterval > 0)
2344            {
2345              long shouldWarnTime =
2346                        passwordExpirationTime - (warningInterval*1000L);
2347              if (shouldWarnTime > currentTime)
2348              {
2349                // The warning time is in the future, so we know the password isn't
2350                // expired.
2351                shouldWarn        = ConditionResult.FALSE;
2352                isFirstWarning    = ConditionResult.FALSE;
2353                isPasswordExpired = ConditionResult.FALSE;
2354              }
2355              else
2356              {
2357                // We're at least in the warning period, but the password may be
2358                // expired.
2359                long warnedTime = getWarnedTime();
2360    
2361                if (passwordExpirationTime > currentTime)
2362                {
2363                  // The password is not expired but we should warn the user.
2364                  shouldWarn        = ConditionResult.TRUE;
2365                  isPasswordExpired = ConditionResult.FALSE;
2366    
2367                  if (warnedTime < 0)
2368                  {
2369                    isFirstWarning = ConditionResult.TRUE;
2370                    setWarnedTime();
2371    
2372                    if (! passwordPolicy.expirePasswordsWithoutWarning())
2373                    {
2374                      passwordExpirationTime =
2375                           currentTime + (warningInterval*1000L);
2376                    }
2377                  }
2378                  else
2379                  {
2380                    isFirstWarning = ConditionResult.FALSE;
2381    
2382                    if (! passwordPolicy.expirePasswordsWithoutWarning())
2383                    {
2384                      passwordExpirationTime = warnedTime + (warningInterval*1000L);
2385                    }
2386                  }
2387                }
2388                else
2389                {
2390                  // The expiration time has passed, but we may not actually be
2391                  // expired if the user has not yet seen a warning.
2392                  if (passwordPolicy.expirePasswordsWithoutWarning())
2393                  {
2394                    shouldWarn        = ConditionResult.FALSE;
2395                    isFirstWarning    = ConditionResult.FALSE;
2396                    isPasswordExpired = ConditionResult.TRUE;
2397                  }
2398                  else if (warnedTime > 0)
2399                  {
2400                    passwordExpirationTime = warnedTime + (warningInterval*1000L);
2401                    if (passwordExpirationTime > currentTime)
2402                    {
2403                      shouldWarn        = ConditionResult.TRUE;
2404                      isFirstWarning    = ConditionResult.FALSE;
2405                      isPasswordExpired = ConditionResult.FALSE;
2406                    }
2407                    else
2408                    {
2409                      shouldWarn        = ConditionResult.FALSE;
2410                      isFirstWarning    = ConditionResult.FALSE;
2411                      isPasswordExpired = ConditionResult.TRUE;
2412                    }
2413                  }
2414                  else
2415                  {
2416                    shouldWarn             = ConditionResult.TRUE;
2417                    isFirstWarning         = ConditionResult.TRUE;
2418                    isPasswordExpired      = ConditionResult.FALSE;
2419                    passwordExpirationTime = currentTime + (warningInterval*1000L);
2420                  }
2421                }
2422              }
2423            }
2424            else
2425            {
2426              // There will never be a warning, and the user's password may be
2427              // expired.
2428              shouldWarn     = ConditionResult.FALSE;
2429              isFirstWarning = ConditionResult.FALSE;
2430    
2431              if (currentTime > passwordExpirationTime)
2432              {
2433                isPasswordExpired = ConditionResult.TRUE;
2434              }
2435              else
2436              {
2437                isPasswordExpired = ConditionResult.FALSE;
2438              }
2439            }
2440          }
2441          else
2442          {
2443            mayUseGraceLogin = ConditionResult.FALSE;
2444            shouldWarn       = ConditionResult.FALSE;
2445            isFirstWarning   = ConditionResult.FALSE;
2446    
2447            if (passwordExpirationTime < currentTime)
2448            {
2449              isPasswordExpired = ConditionResult.TRUE;
2450            }
2451            else
2452            {
2453              isPasswordExpired = ConditionResult.FALSE;
2454            }
2455          }
2456        }
2457    
2458        if (debugEnabled())
2459        {
2460          TRACER.debugInfo("Returning password expiration time of %d for user " +
2461              "%s.", passwordExpirationTime, userDNString);
2462        }
2463    
2464        return passwordExpirationTime;
2465      }
2466    
2467    
2468    
2469      /**
2470       * Indicates whether the user's password is currently expired.
2471       *
2472       * @return  <CODE>true</CODE> if the user's password is currently expired, or
2473       *          <CODE>false</CODE> if not.
2474       */
2475      public boolean isPasswordExpired()
2476      {
2477        if ((isPasswordExpired == null) ||
2478            (isPasswordExpired == ConditionResult.UNDEFINED))
2479        {
2480          getPasswordExpirationTime();
2481        }
2482    
2483        return isPasswordExpired == ConditionResult.TRUE;
2484      }
2485    
2486    
2487    
2488      /**
2489       * Indicates whether the user's last password change was within the minimum
2490       * password age.
2491       *
2492       * @return  <CODE>true</CODE> if the password minimum age is nonzero, the
2493       *          account is not in force-change mode, and the last password change
2494       *          was within the minimum age, or <CODE>false</CODE> otherwise.
2495       */
2496      public boolean isWithinMinimumAge()
2497      {
2498        // This feature is reponsible for neither a state field nor entry state
2499        // attribute.
2500        int minAge = passwordPolicy.getMinimumPasswordAge();
2501        if (minAge <= 0)
2502        {
2503          // There is no minimum age, so the user isn't in it.
2504          if (debugEnabled())
2505          {
2506            TRACER.debugInfo("Returning false because there is no minimum age.");
2507          }
2508    
2509          return false;
2510        }
2511        else if ((passwordChangedTime + (minAge*1000L)) < currentTime)
2512        {
2513          // It's been long enough since the user changed their password.
2514          if (debugEnabled())
2515          {
2516            TRACER.debugInfo("Returning false because the minimum age has " +
2517                "expired.");
2518          }
2519    
2520          return false;
2521        }
2522        else if (mustChangePassword())
2523        {
2524          // The user is in a must-change mode, so the minimum age doesn't apply.
2525          if (debugEnabled())
2526          {
2527            TRACER.debugInfo("Returning false because the account is in a " +
2528                "must-change state.");
2529          }
2530    
2531          return false;
2532        }
2533        else
2534        {
2535          // The user is within the minimum age.
2536          if (debugEnabled())
2537          {
2538            TRACER.debugInfo("Returning true.");
2539          }
2540    
2541          return true;
2542        }
2543      }
2544    
2545    
2546    
2547      /**
2548       * Indicates whether the user may use a grace login if the password is expired
2549       * and there is at least one grace login remaining.  Note that this does not
2550       * check to see if the user's password is expired, does not verify that there
2551       * are any remaining grace logins, and does not update the set of grace login
2552       * times.
2553       *
2554       * @return  <CODE>true</CODE> if the user may use a grace login if the
2555       *          password is expired and there is at least one grace login
2556       *          remaining, or <CODE>false</CODE> if the user may not use a grace
2557       *          login for some reason.
2558       */
2559      public boolean mayUseGraceLogin()
2560      {
2561        if ((mayUseGraceLogin == null) ||
2562            (mayUseGraceLogin == ConditionResult.UNDEFINED))
2563        {
2564          getPasswordExpirationTime();
2565        }
2566    
2567        return mayUseGraceLogin == ConditionResult.TRUE;
2568      }
2569    
2570    
2571    
2572      /**
2573       * Indicates whether the user should receive a warning notification that the
2574       * password is about to expire.
2575       *
2576       * @return  <CODE>true</CODE> if the user should receive a warning
2577       *          notification that the password is about to expire, or
2578       *          <CODE>false</CODE> if not.
2579       */
2580      public boolean shouldWarn()
2581      {
2582        if ((shouldWarn == null) || (shouldWarn == ConditionResult.UNDEFINED))
2583        {
2584          getPasswordExpirationTime();
2585        }
2586    
2587        return shouldWarn == ConditionResult.TRUE;
2588      }
2589    
2590    
2591    
2592      /**
2593       * Indicates whether the warning that the user should receive would be the
2594       * first warning for the user.
2595       *
2596       * @return  <CODE>true</CODE> if the warning that should be sent to the user
2597       *          would be the first warning, or <CODE>false</CODE> if not.
2598       */
2599      public boolean isFirstWarning()
2600      {
2601        if ((isFirstWarning == null) ||
2602            (isFirstWarning == ConditionResult.UNDEFINED))
2603        {
2604          getPasswordExpirationTime();
2605        }
2606    
2607        return isFirstWarning == ConditionResult.TRUE;
2608      }
2609    
2610    
2611    
2612      /**
2613       * Retrieves the length of time in seconds until the user's password expires.
2614       *
2615       * @return  The length of time in seconds until the user's password expires,
2616       *          0 if the password is currently expired, or -1 if the password
2617       *          should not expire.
2618       */
2619      public int getSecondsUntilExpiration()
2620      {
2621        long expirationTime = getPasswordExpirationTime();
2622        if (expirationTime < 0)
2623        {
2624          return -1;
2625        }
2626        else if (expirationTime < currentTime)
2627        {
2628          return 0;
2629        }
2630        else
2631        {
2632          return (int) ((expirationTime - currentTime) / 1000);
2633        }
2634      }
2635    
2636    
2637    
2638      /**
2639       * Retrieves the timestamp for the last required change time that the user
2640       * complied with.
2641       *
2642       * @return  The timestamp for the last required change time that the user
2643       *          complied with, or -1 if the user's password has not been changed
2644       *          in compliance with this configuration.
2645       */
2646      public long getRequiredChangeTime()
2647      {
2648        if (requiredChangeTime != Long.MIN_VALUE)
2649        {
2650          if (debugEnabled())
2651          {
2652            TRACER.debugInfo("Returning stored required change time of %d for " +
2653                "user %s", requiredChangeTime, userDNString);
2654          }
2655    
2656          return requiredChangeTime;
2657        }
2658    
2659        AttributeType type = DirectoryServer.getAttributeType(
2660                                  OP_ATTR_PWPOLICY_CHANGED_BY_REQUIRED_TIME, true);
2661    
2662        try
2663        {
2664          requiredChangeTime = getGeneralizedTime(type);
2665        }
2666        catch (Exception e)
2667        {
2668          if (debugEnabled())
2669          {
2670            TRACER.debugCaught(DebugLogLevel.ERROR, e);
2671          }
2672    
2673          requiredChangeTime = -1;
2674          if (debugEnabled())
2675          {
2676            TRACER.debugWarning("Returning %d for user %s because an error " +
2677                "occurred: %s", requiredChangeTime, userDNString,
2678                         stackTraceToSingleLineString(e));
2679          }
2680    
2681          return requiredChangeTime;
2682        }
2683    
2684        if (debugEnabled())
2685        {
2686          TRACER.debugInfo("Returning required change time of %d for user %s",
2687              requiredChangeTime, userDNString);
2688        }
2689    
2690        return requiredChangeTime;
2691      }
2692    
2693    
2694    
2695      /**
2696       * Updates the user entry with a timestamp indicating that the password has
2697       * been changed in accordance with the require change time.
2698       */
2699      public void setRequiredChangeTime()
2700      {
2701        long requiredChangeByTimePolicy = passwordPolicy.getRequireChangeByTime();
2702        if (requiredChangeByTimePolicy > 0)
2703        {
2704          setRequiredChangeTime(requiredChangeByTimePolicy);
2705        }
2706      }
2707    
2708    
2709    
2710      /**
2711       * Updates the user entry with a timestamp indicating that the password has
2712       * been changed in accordance with the require change time.
2713       *
2714       * @param  requiredChangeTime  The timestamp to use for the required change
2715       *                             time value.
2716       */
2717      public void setRequiredChangeTime(long requiredChangeTime)
2718      {
2719        if (debugEnabled())
2720        {
2721          TRACER.debugInfo("Updating required change time for user %s",
2722              userDNString);
2723        }
2724    
2725        if (getRequiredChangeTime() != requiredChangeTime)
2726        {
2727          AttributeType type = DirectoryServer.getAttributeType(
2728                                   OP_ATTR_PWPOLICY_CHANGED_BY_REQUIRED_TIME, true);
2729    
2730          LinkedHashSet<AttributeValue> values =
2731               new LinkedHashSet<AttributeValue>(1);
2732          String timeValue = GeneralizedTimeSyntax.format(requiredChangeTime);
2733          values.add(new AttributeValue(type, timeValue));
2734    
2735          Attribute a = new Attribute(type,
2736                                      OP_ATTR_PWPOLICY_CHANGED_BY_REQUIRED_TIME,
2737                                      values);
2738    
2739          if (updateEntry)
2740          {
2741            ArrayList<Attribute> attrList = new ArrayList<Attribute>(1);
2742            attrList.add(a);
2743            userEntry.putAttribute(type, attrList);
2744          }
2745          else
2746          {
2747            modifications.add(new Modification(ModificationType.REPLACE, a, true));
2748          }
2749        }
2750      }
2751    
2752    
2753    
2754      /**
2755       * Updates the user entry to remove any timestamp indicating that the password
2756       * has been changed in accordance with the required change time.
2757       */
2758      public void clearRequiredChangeTime()
2759      {
2760        if (debugEnabled())
2761        {
2762          TRACER.debugInfo("Clearing required change time for user %s",
2763              userDNString);
2764        }
2765    
2766        AttributeType type = DirectoryServer.getAttributeType(
2767                                 OP_ATTR_PWPOLICY_CHANGED_BY_REQUIRED_TIME, true);
2768        if (updateEntry)
2769        {
2770          userEntry.removeAttribute(type);
2771        }
2772        else
2773        {
2774          modifications.add(new Modification(ModificationType.REPLACE,
2775                                             new Attribute(type), true));
2776        }
2777      }
2778    
2779    
2780    
2781      /**
2782       * Retrieves the time that the user was first warned about an upcoming
2783       * expiration.
2784       *
2785       * @return  The time that the user was first warned about an upcoming
2786       *          expiration, or -1 if the user has not been warned.
2787       */
2788      public long getWarnedTime()
2789      {
2790        if (warnedTime == Long.MIN_VALUE)
2791        {
2792          AttributeType type =
2793               DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_WARNED_TIME, true);
2794          try
2795          {
2796            warnedTime = getGeneralizedTime(type);
2797          }
2798          catch (Exception e)
2799          {
2800            if (debugEnabled())
2801            {
2802              TRACER.debugCaught(DebugLogLevel.ERROR, e);
2803            }
2804    
2805            if (debugEnabled())
2806            {
2807              TRACER.debugWarning("Unable to decode the warned time for user %s: " +
2808                  "%s", userDNString, stackTraceToSingleLineString(e));
2809            }
2810    
2811            warnedTime = -1;
2812          }
2813        }
2814    
2815    
2816        if (debugEnabled())
2817        {
2818          TRACER.debugInfo("Returning a warned time of %d for user %s",
2819              warnedTime, userDNString);
2820        }
2821    
2822        return warnedTime;
2823      }
2824    
2825    
2826    
2827      /**
2828       * Updates the user entry to set the warned time to the current time.
2829       */
2830      public void setWarnedTime()
2831      {
2832        setWarnedTime(currentTime);
2833      }
2834    
2835    
2836    
2837      /**
2838       * Updates the user entry to set the warned time to the specified time.  This
2839       * method should generally only be used for testing purposes, since the
2840       * variant that uses the current time is preferred almost everywhere else.
2841       *
2842       * @param  warnedTime  The value to use for the warned time.
2843       */
2844      public void setWarnedTime(long warnedTime)
2845      {
2846        long warnTime = getWarnedTime();
2847        if (warnTime == warnedTime)
2848        {
2849          if (debugEnabled())
2850          {
2851            TRACER.debugInfo("Not updating warned time for user %s because " +
2852                "the warned time is the same as the specified time.",
2853                userDNString);
2854          }
2855    
2856          return;
2857        }
2858    
2859        this.warnedTime = warnedTime;
2860    
2861        AttributeType type =
2862             DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_WARNED_TIME, true);
2863        LinkedHashSet<AttributeValue> values = new LinkedHashSet<AttributeValue>(1);
2864        values.add(GeneralizedTimeSyntax.createGeneralizedTimeValue(currentTime));
2865    
2866        Attribute a = new Attribute(type, OP_ATTR_PWPOLICY_WARNED_TIME, values);
2867    
2868        if (updateEntry)
2869        {
2870          ArrayList<Attribute> attrList = new ArrayList<Attribute>(1);
2871          attrList.add(a);
2872          userEntry.putAttribute(type, attrList);
2873        }
2874        else
2875        {
2876          modifications.add(new Modification(ModificationType.REPLACE, a, true));
2877        }
2878    
2879        if (debugEnabled())
2880        {
2881          TRACER.debugInfo("Updated the warned time for user %s", userDNString);
2882        }
2883      }
2884    
2885    
2886    
2887      /**
2888       * Updates the user entry to clear the warned time.
2889       */
2890      public void clearWarnedTime()
2891      {
2892        if (debugEnabled())
2893        {
2894          TRACER.debugInfo("Clearing warned time for user %s", userDNString);
2895        }
2896    
2897        if (getWarnedTime() < 0)
2898        {
2899          return;
2900        }
2901        warnedTime = -1;
2902    
2903        AttributeType type =
2904             DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_WARNED_TIME, true);
2905        if (updateEntry)
2906        {
2907          userEntry.removeAttribute(type);
2908        }
2909        else
2910        {
2911          Attribute a = new Attribute(type);
2912          modifications.add(new Modification(ModificationType.REPLACE, a, true));
2913        }
2914    
2915        if (debugEnabled())
2916        {
2917          TRACER.debugInfo("Cleared the warned time for user %s", userDNString);
2918        }
2919      }
2920    
2921    
2922    
2923      /**
2924       * Retrieves the times that the user has authenticated to the server using a
2925       * grace login.
2926       *
2927       * @return  The times that the user has authenticated to the server using a
2928       *          grace login.
2929       */
2930      public List<Long> getGraceLoginTimes()
2931      {
2932        if (graceLoginTimes == null)
2933        {
2934          AttributeType type = DirectoryServer.getAttributeType(
2935                                    OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME_LC);
2936          if (type == null)
2937          {
2938            type = DirectoryServer.getDefaultAttributeType(
2939                                        OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME);
2940          }
2941    
2942          try
2943          {
2944            graceLoginTimes = getGeneralizedTimes(type);
2945          }
2946          catch (Exception e)
2947          {
2948            if (debugEnabled())
2949            {
2950              TRACER.debugCaught(DebugLogLevel.ERROR, e);
2951            }
2952    
2953            if (debugEnabled())
2954            {
2955              TRACER.debugWarning("Error while processing grace login times " +
2956                   "for user %s: %s",
2957                           userDNString, stackTraceToSingleLineString(e));
2958            }
2959    
2960            graceLoginTimes = new ArrayList<Long>();
2961    
2962            if (updateEntry)
2963            {
2964              userEntry.removeAttribute(type);
2965            }
2966            else
2967            {
2968              modifications.add(new Modification(ModificationType.REPLACE,
2969                                                 new Attribute(type), true));
2970            }
2971          }
2972        }
2973    
2974    
2975        if (debugEnabled())
2976        {
2977          TRACER.debugInfo("Returning grace login times for user %s",
2978              userDNString);
2979        }
2980    
2981        return graceLoginTimes;
2982      }
2983    
2984    
2985    
2986      /**
2987       * Retrieves the number of grace logins that the user has left.
2988       *
2989       * @return  The number of grace logins that the user has left, or -1 if grace
2990       *          logins are not allowed.
2991       */
2992      public int getGraceLoginsRemaining()
2993      {
2994        int maxGraceLogins = passwordPolicy.getGraceLoginCount();
2995        if (maxGraceLogins <= 0)
2996        {
2997          return -1;
2998        }
2999    
3000        List<Long> graceLoginTimes = getGraceLoginTimes();
3001        return maxGraceLogins - graceLoginTimes.size();
3002      }
3003    
3004    
3005    
3006      /**
3007       * Updates the set of grace login times for the user to include the current
3008       * time.
3009       */
3010      public void updateGraceLoginTimes()
3011      {
3012        if (debugEnabled())
3013        {
3014          TRACER.debugInfo("Updating grace login times for user %s",
3015              userDNString);
3016        }
3017    
3018        List<Long> graceTimes = getGraceLoginTimes();
3019        long highestGraceTime = -1;
3020        for (Long l : graceTimes)
3021        {
3022          highestGraceTime = Math.max(l, highestGraceTime);
3023        }
3024    
3025        if (highestGraceTime >= currentTime)
3026        {
3027          highestGraceTime++;
3028        }
3029        else
3030        {
3031          highestGraceTime = currentTime;
3032        }
3033        graceTimes.add(highestGraceTime); // graceTimes == this.graceLoginTimes
3034    
3035        AttributeType type =
3036             DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME_LC);
3037        if (type == null)
3038        {
3039          type = DirectoryServer.getDefaultAttributeType(
3040                                      OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME);
3041        }
3042    
3043        if (updateEntry)
3044        {
3045          LinkedHashSet<AttributeValue> values =
3046                 new LinkedHashSet<AttributeValue>(graceTimes.size());
3047          for (Long l : graceTimes)
3048          {
3049            values.add(new AttributeValue(type, GeneralizedTimeSyntax.format(l)));
3050          }
3051    
3052          Attribute a = new Attribute(type, OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME,
3053                                      values);
3054          ArrayList<Attribute> attrList = new ArrayList<Attribute>(1);
3055          attrList.add(a);
3056    
3057          userEntry.putAttribute(type, attrList);
3058        }
3059        else
3060        {
3061          LinkedHashSet<AttributeValue> addValues =
3062               new LinkedHashSet<AttributeValue>(1);
3063          addValues.add(new AttributeValue(type,
3064                                 GeneralizedTimeSyntax.format(highestGraceTime)));
3065          Attribute addAttr = new Attribute(type, OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME,
3066                                            addValues);
3067    
3068          modifications.add(new Modification(ModificationType.ADD, addAttr, true));
3069        }
3070      }
3071    
3072    
3073    
3074      /**
3075       * Specifies the set of grace login use times for the associated user.  If
3076       * the provided list is empty or {@code null}, then the set will be cleared.
3077       *
3078       * @param  graceLoginTimes  The grace login use times for the associated user.
3079       */
3080      public void setGraceLoginTimes(List<Long> graceLoginTimes)
3081      {
3082        if ((graceLoginTimes == null) || graceLoginTimes.isEmpty())
3083        {
3084          clearGraceLoginTimes();
3085          return;
3086        }
3087    
3088        if (debugEnabled())
3089        {
3090          TRACER.debugInfo("Updating grace login times for user %s",
3091              userDNString);
3092        }
3093    
3094        AttributeType type =
3095             DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME_LC,
3096                                              true);
3097        LinkedHashSet<AttributeValue> values =
3098             new LinkedHashSet<AttributeValue>(graceLoginTimes.size());
3099        for (Long l : graceLoginTimes)
3100        {
3101          values.add(new AttributeValue(type, GeneralizedTimeSyntax.format(l)));
3102        }
3103        Attribute a =
3104             new Attribute(type, OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME, values);
3105    
3106        if (updateEntry)
3107        {
3108          ArrayList<Attribute> attrList = new ArrayList<Attribute>(1);
3109          attrList.add(a);
3110    
3111          userEntry.putAttribute(type, attrList);
3112        }
3113        else
3114        {
3115          modifications.add(new Modification(ModificationType.REPLACE, a, true));
3116        }
3117      }
3118    
3119    
3120    
3121      /**
3122       * Updates the user entry to remove any record of previous grace logins.
3123       */
3124      public void clearGraceLoginTimes()
3125      {
3126        if (debugEnabled())
3127        {
3128          TRACER.debugInfo("Clearing grace login times for user %s",
3129              userDNString);
3130        }
3131    
3132        List<Long> graceTimes = getGraceLoginTimes();
3133        if (graceTimes.isEmpty())
3134        {
3135          return;
3136        }
3137        graceTimes.clear(); // graceTimes == this.graceLoginTimes
3138    
3139        AttributeType type =
3140             DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME_LC);
3141        if (type == null)
3142        {
3143          type = DirectoryServer.getDefaultAttributeType(
3144                                      OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME);
3145        }
3146    
3147        if (updateEntry)
3148        {
3149          userEntry.removeAttribute(type);
3150        }
3151        else
3152        {
3153          modifications.add(new Modification(ModificationType.REPLACE,
3154                                             new Attribute(type), true));
3155        }
3156      }
3157    
3158    
3159    
3160      /**
3161       * Retrieves a list of the clear-text passwords for the user.  If the user
3162       * does not have any passwords in the clear, then the list will be empty.
3163       *
3164       * @return  A list of the clear-text passwords for the user.
3165       */
3166      public List<ByteString> getClearPasswords()
3167      {
3168        LinkedList<ByteString> clearPasswords = new LinkedList<ByteString>();
3169    
3170        List<Attribute> attrList =
3171             userEntry.getAttribute(passwordPolicy.getPasswordAttribute());
3172    
3173        if (attrList == null)
3174        {
3175          return clearPasswords;
3176        }
3177    
3178        for (Attribute a : attrList)
3179        {
3180          boolean usesAuthPasswordSyntax = passwordPolicy.usesAuthPasswordSyntax();
3181    
3182          for (AttributeValue v : a.getValues())
3183          {
3184            try
3185            {
3186              StringBuilder[] pwComponents;
3187              if (usesAuthPasswordSyntax)
3188              {
3189                pwComponents =
3190                     AuthPasswordSyntax.decodeAuthPassword(v.getStringValue());
3191              }
3192              else
3193              {
3194                String[] userPwComponents =
3195                     UserPasswordSyntax.decodeUserPassword(v.getStringValue());
3196                pwComponents = new StringBuilder[userPwComponents.length];
3197                for (int i = 0; i < userPwComponents.length; ++i)
3198                {
3199                  pwComponents[i] = new StringBuilder(userPwComponents[i]);
3200                }
3201              }
3202    
3203              String schemeName = pwComponents[0].toString();
3204              PasswordStorageScheme scheme = (usesAuthPasswordSyntax)
3205                        ? DirectoryServer.getAuthPasswordStorageScheme(schemeName)
3206                        : DirectoryServer.getPasswordStorageScheme(schemeName);
3207              if (scheme == null)
3208              {
3209                if (debugEnabled())
3210                {
3211                  TRACER.debugWarning("User entry %s contains a password with " +
3212                      "scheme %s that is not defined in the server.",
3213                                      userDNString, schemeName);
3214                }
3215    
3216                continue;
3217              }
3218    
3219              if (scheme.isReversible())
3220              {
3221                ByteString clearValue = (usesAuthPasswordSyntax)
3222                             ? scheme.getAuthPasswordPlaintextValue(
3223                                   pwComponents[1].toString(),
3224                                   pwComponents[2].toString())
3225                             : scheme.getPlaintextValue(
3226                                   new ASN1OctetString(pwComponents[1].toString()));
3227                clearPasswords.add(clearValue);
3228              }
3229            }
3230            catch (Exception e)
3231            {
3232              if (debugEnabled())
3233              {
3234                TRACER.debugCaught(DebugLogLevel.ERROR, e);
3235              }
3236    
3237              if (debugEnabled())
3238              {
3239                TRACER.debugWarning("Cannot get clear password value foruser %s: " +
3240                    "%s", userDNString, e);
3241              }
3242            }
3243          }
3244        }
3245    
3246        return clearPasswords;
3247      }
3248    
3249    
3250    
3251      /**
3252       * Indicates whether the provided password value matches any of the stored
3253       * passwords in the user entry.
3254       *
3255       * @param  password  The user-provided password to verify.
3256       *
3257       * @return  <CODE>true</CODE> if the provided password matches any of the
3258       *          stored password values, or <CODE>false</CODE> if not.
3259       */
3260      public boolean passwordMatches(ByteString password)
3261      {
3262        List<Attribute> attrList =
3263             userEntry.getAttribute(passwordPolicy.getPasswordAttribute());
3264        if ((attrList == null) || attrList.isEmpty())
3265        {
3266          if (debugEnabled())
3267          {
3268            TRACER.debugInfo("Returning false because user %s does not have " +
3269                "any values for password attribute %s", userDNString,
3270                passwordPolicy.getPasswordAttribute().getNameOrOID());
3271          }
3272    
3273          return false;
3274        }
3275    
3276        for (Attribute a : attrList)
3277        {
3278          boolean usesAuthPasswordSyntax = passwordPolicy.usesAuthPasswordSyntax();
3279    
3280          for (AttributeValue v : a.getValues())
3281          {
3282            try
3283            {
3284              StringBuilder[] pwComponents;
3285              if (usesAuthPasswordSyntax)
3286              {
3287                pwComponents =
3288                     AuthPasswordSyntax.decodeAuthPassword(v.getStringValue());
3289              }
3290              else
3291              {
3292                String[] userPwComponents =
3293                     UserPasswordSyntax.decodeUserPassword(v.getStringValue());
3294                pwComponents = new StringBuilder[userPwComponents.length];
3295                for (int i = 0; i < userPwComponents.length; ++i)
3296                {
3297                  pwComponents[i] = new StringBuilder(userPwComponents[i]);
3298                }
3299              }
3300    
3301              String schemeName = pwComponents[0].toString();
3302              PasswordStorageScheme scheme = (usesAuthPasswordSyntax)
3303                         ? DirectoryServer.getAuthPasswordStorageScheme(schemeName)
3304                         : DirectoryServer.getPasswordStorageScheme(schemeName);
3305              if (scheme == null)
3306              {
3307                if (debugEnabled())
3308                {
3309                  TRACER.debugWarning("User entry %s contains a password with " +
3310                      "scheme %s that is not defined in the server.",
3311                                      userDNString, schemeName);
3312                }
3313    
3314                continue;
3315              }
3316    
3317              boolean passwordMatches = (usesAuthPasswordSyntax)
3318                         ? scheme.authPasswordMatches(password,
3319                                                      pwComponents[1].toString(),
3320                                                      pwComponents[2].toString())
3321                         : scheme.passwordMatches(password,
3322                                   new ASN1OctetString(pwComponents[1].toString()));
3323              if (passwordMatches)
3324              {
3325                if (debugEnabled())
3326                {
3327                  TRACER.debugInfo("Returning true for user %s because the " +
3328                      "provided password matches a value encoded with scheme %s",
3329                      userDNString, schemeName);
3330                }
3331    
3332                return true;
3333              }
3334            }
3335            catch (Exception e)
3336            {
3337              if (debugEnabled())
3338              {
3339                TRACER.debugCaught(DebugLogLevel.ERROR, e);
3340              }
3341    
3342              if (debugEnabled())
3343              {
3344                TRACER.debugWarning("An error occurred while attempting to " +
3345                    "process a password value for user %s: %s",
3346                         userDNString, stackTraceToSingleLineString(e));
3347              }
3348            }
3349          }
3350        }
3351    
3352        // If we've gotten here, then we couldn't find a match.
3353        if (debugEnabled())
3354        {
3355          TRACER.debugInfo("Returning false because the provided password does " +
3356              "not match any of the stored password values for user %s",
3357              userDNString);
3358        }
3359    
3360        return false;
3361      }
3362    
3363    
3364    
3365      /**
3366       * Indicates whether the provided password value is pre-encoded.
3367       *
3368       * @param  passwordValue  The value for which to make the determination.
3369       *
3370       * @return  <CODE>true</CODE> if the provided password value is pre-encoded,
3371       *          or <CODE>false</CODE> if it is not.
3372       */
3373      public boolean passwordIsPreEncoded(ByteString passwordValue)
3374      {
3375        if (passwordPolicy.usesAuthPasswordSyntax())
3376        {
3377          return AuthPasswordSyntax.isEncoded(passwordValue);
3378        }
3379        else
3380        {
3381          return UserPasswordSyntax.isEncoded(passwordValue);
3382        }
3383      }
3384    
3385    
3386    
3387      /**
3388       * Encodes the provided password using the default storage schemes (using the
3389       * appropriate syntax for the password attribute).
3390       *
3391       * @param  password  The password to be encoded.
3392       *
3393       * @return  The password encoded using the default schemes.
3394       *
3395       * @throws  DirectoryException  If a problem occurs while attempting to encode
3396       *                              the password.
3397       */
3398      public List<ByteString> encodePassword(ByteString password)
3399             throws DirectoryException
3400      {
3401        List<PasswordStorageScheme> schemes =
3402             passwordPolicy.getDefaultStorageSchemes();
3403        List<ByteString> encodedPasswords =
3404             new ArrayList<ByteString>(schemes.size());
3405    
3406        if (passwordPolicy.usesAuthPasswordSyntax())
3407        {
3408          for (PasswordStorageScheme s : schemes)
3409          {
3410            encodedPasswords.add(s.encodeAuthPassword(password));
3411          }
3412        }
3413        else
3414        {
3415          for (PasswordStorageScheme s : schemes)
3416          {
3417            encodedPasswords.add(s.encodePasswordWithScheme(password));
3418          }
3419        }
3420    
3421        return encodedPasswords;
3422      }
3423    
3424    
3425    
3426      /**
3427       * Indicates whether the provided password appears to be acceptable according
3428       * to the password validators.
3429       *
3430       * @param  operation         The operation that provided the password.
3431       * @param  userEntry         The user entry in which the password is used.
3432       * @param  newPassword       The password to be validated.
3433       * @param  currentPasswords  The set of clear-text current passwords for the
3434       *                           user (this may be a subset if not all of them are
3435       *                           available in the clear, or empty if none of them
3436       *                           are available in the clear).
3437       * @param  invalidReason     A buffer that may be used to hold the invalid
3438       *                           reason if the password is rejected.
3439       *
3440       * @return  <CODE>true</CODE> if the password is acceptable for use, or
3441       *          <CODE>false</CODE> if it is not.
3442       */
3443      public boolean passwordIsAcceptable(Operation operation, Entry userEntry,
3444                                          ByteString newPassword,
3445                                          Set<ByteString> currentPasswords,
3446                                          MessageBuilder invalidReason)
3447      {
3448        for (DN validatorDN : passwordPolicy.getPasswordValidators().keySet())
3449        {
3450          PasswordValidator<? extends PasswordValidatorCfg> validator =
3451               passwordPolicy.getPasswordValidators().get(validatorDN);
3452    
3453          if (! validator.passwordIsAcceptable(newPassword, currentPasswords,
3454                                               operation, userEntry, invalidReason))
3455          {
3456            if (debugEnabled())
3457            {
3458              TRACER.debugInfo("The password provided for user %s failed " +
3459                  "the %s password validator.",
3460                  userDNString, validatorDN.toString());
3461            }
3462    
3463            return false;
3464          }
3465          else
3466          {
3467            if (debugEnabled())
3468            {
3469              TRACER.debugInfo("The password provided for user %s passed " +
3470                  "the %s password validator.",
3471                  userDNString, validatorDN.toString());
3472            }
3473          }
3474        }
3475    
3476        return true;
3477      }
3478    
3479    
3480    
3481      /**
3482       * Performs any processing that may be necessary to remove deprecated storage
3483       * schemes from the user's entry that match the provided password and
3484       * re-encodes them using the default schemes.
3485       *
3486       * @param  password  The clear-text password provided by the user.
3487       */
3488      public void handleDeprecatedStorageSchemes(ByteString password)
3489      {
3490        if (passwordPolicy.getDefaultStorageSchemes().isEmpty())
3491        {
3492          if (debugEnabled())
3493          {
3494            TRACER.debugInfo("Doing nothing for user %s because no " +
3495                "deprecated storage schemes have been defined.", userDNString);
3496          }
3497    
3498          return;
3499        }
3500    
3501    
3502        AttributeType type = passwordPolicy.getPasswordAttribute();
3503        List<Attribute> attrList = userEntry.getAttribute(type);
3504        if ((attrList == null) || attrList.isEmpty())
3505        {
3506          if (debugEnabled())
3507          {
3508            TRACER.debugInfo("Doing nothing for entry %s because no password " +
3509                "values were found.", userDNString);
3510          }
3511    
3512          return;
3513        }
3514    
3515    
3516        HashSet<String> existingDefaultSchemes = new HashSet<String>();
3517        LinkedHashSet<AttributeValue> removedValues =
3518             new LinkedHashSet<AttributeValue>();
3519        LinkedHashSet<AttributeValue> updatedValues =
3520             new LinkedHashSet<AttributeValue>();
3521    
3522        boolean usesAuthPasswordSyntax = passwordPolicy.usesAuthPasswordSyntax();
3523    
3524        for (Attribute a : attrList)
3525        {
3526          Iterator<AttributeValue> iterator = a.getValues().iterator();
3527          while (iterator.hasNext())
3528          {
3529            AttributeValue v = iterator.next();
3530    
3531            try
3532            {
3533              StringBuilder[] pwComponents;
3534              if (usesAuthPasswordSyntax)
3535              {
3536                pwComponents =
3537                     AuthPasswordSyntax.decodeAuthPassword(v.getStringValue());
3538              }
3539              else
3540              {
3541                String[] userPwComponents =
3542                     UserPasswordSyntax.decodeUserPassword(v.getStringValue());
3543                pwComponents = new StringBuilder[userPwComponents.length];
3544                for (int i = 0; i < userPwComponents.length; ++i)
3545                {
3546                  pwComponents[i] = new StringBuilder(userPwComponents[i]);
3547                }
3548              }
3549    
3550              String schemeName = pwComponents[0].toString();
3551              PasswordStorageScheme scheme = (usesAuthPasswordSyntax)
3552                        ? DirectoryServer.getAuthPasswordStorageScheme(schemeName)
3553                        : DirectoryServer.getPasswordStorageScheme(schemeName);
3554              if (scheme == null)
3555              {
3556                if (debugEnabled())
3557                {
3558                  TRACER.debugWarning("Skipping password value for user %s " +
3559                      "because the associated storage scheme %s is not " +
3560                      "configured for use.", userDNString, schemeName);
3561                }
3562    
3563                continue;
3564              }
3565    
3566              boolean passwordMatches = (usesAuthPasswordSyntax)
3567                         ? scheme.authPasswordMatches(password,
3568                                                      pwComponents[1].toString(),
3569                                                      pwComponents[2].toString())
3570                         : scheme.passwordMatches(password,
3571                                   new ASN1OctetString(pwComponents[1].toString()));
3572              if (passwordMatches)
3573              {
3574                if (passwordPolicy.isDefaultStorageScheme(schemeName))
3575                {
3576                  existingDefaultSchemes.add(schemeName);
3577                  updatedValues.add(v);
3578                }
3579                else if (passwordPolicy.isDeprecatedStorageScheme(schemeName))
3580                {
3581                  if (debugEnabled())
3582                  {
3583                    TRACER.debugInfo("Marking password with scheme %s for " +
3584                        "removal from user entry %s.", schemeName, userDNString);
3585                  }
3586    
3587                  iterator.remove();
3588                  removedValues.add(v);
3589                }
3590                else
3591                {
3592                  updatedValues.add(v);
3593                }
3594              }
3595            }
3596            catch (Exception e)
3597            {
3598              if (debugEnabled())
3599              {
3600                TRACER.debugCaught(DebugLogLevel.ERROR, e);
3601    
3602                TRACER.debugWarning("Skipping password value for user %s because " +
3603                    "an error occurred while attempting to decode it based on " +
3604                    "the user password syntax: %s",
3605                    userDNString, stackTraceToSingleLineString(e));
3606              }
3607            }
3608          }
3609        }
3610    
3611        if (removedValues.isEmpty())
3612        {
3613          if (debugEnabled())
3614          {
3615            TRACER.debugInfo("User entry %s does not have any password values " +
3616                "encoded using deprecated schemes.", userDNString);
3617          }
3618    
3619          return;
3620        }
3621    
3622        LinkedHashSet<AttributeValue> addedValues = new
3623             LinkedHashSet<AttributeValue>();
3624        for (PasswordStorageScheme s :
3625             passwordPolicy.getDefaultStorageSchemes())
3626        {
3627          if (! existingDefaultSchemes.contains(
3628               toLowerCase(s.getStorageSchemeName())))
3629          {
3630            try
3631            {
3632              ByteString encodedPassword = (usesAuthPasswordSyntax)
3633                                           ? s.encodeAuthPassword(password)
3634                                           : s.encodePasswordWithScheme(password);
3635              AttributeValue v = new AttributeValue(type, encodedPassword);
3636              addedValues.add(v);
3637              updatedValues.add(v);
3638            }
3639            catch (Exception e)
3640            {
3641              if (debugEnabled())
3642              {
3643                TRACER.debugCaught(DebugLogLevel.ERROR, e);
3644              }
3645    
3646              if (debugEnabled())
3647              {
3648                TRACER.debugWarning("Unable to encode password for user %s using " +
3649                     "default scheme %s: %s",
3650                             userDNString, s.getStorageSchemeName(),
3651                             stackTraceToSingleLineString(e));
3652              }
3653            }
3654          }
3655        }
3656    
3657        if (updatedValues.isEmpty())
3658        {
3659          if (debugEnabled())
3660          {
3661            TRACER.debugWarning("Not updating user entry %s because removing " +
3662                 "deprecated schemes would leave the user without a password.",
3663                         userDNString);
3664          }
3665    
3666          return;
3667        }
3668    
3669        if (updateEntry)
3670        {
3671          ArrayList<Attribute> newList = new ArrayList<Attribute>(1);
3672          newList.add(new Attribute(type, type.getNameOrOID(), updatedValues));
3673          userEntry.putAttribute(type, newList);
3674        }
3675        else
3676        {
3677          Attribute a = new Attribute(type, type.getNameOrOID(), removedValues);
3678          modifications.add(new Modification(ModificationType.DELETE, a, true));
3679    
3680          if (! addedValues.isEmpty())
3681          {
3682            Attribute a2 = new Attribute(type, type.getNameOrOID(), addedValues);
3683            modifications.add(new Modification(ModificationType.ADD, a2, true));
3684          }
3685        }
3686    
3687        if (debugEnabled())
3688        {
3689          TRACER.debugInfo("Updating user entry %s to replace password values " +
3690              "encoded with deprecated schemes with values encoded " +
3691              "with the default schemes.", userDNString);
3692        }
3693      }
3694    
3695    
3696    
3697      /**
3698       * Indicates whether password history information should be matained for this
3699       * user.
3700       *
3701       * @return  {@code true} if password history information should be maintained
3702       *          for this user, or {@code false} if not.
3703       */
3704      public boolean maintainHistory()
3705      {
3706        return ((passwordPolicy.getPasswordHistoryCount() > 0) ||
3707                (passwordPolicy.getPasswordHistoryDuration() > 0));
3708      }
3709    
3710    
3711    
3712      /**
3713       * Indicates whether the provided password is equal to any of the current
3714       * passwords, or any of the passwords in the history.
3715       *
3716       * @param  password  The password for which to make the determination.
3717       *
3718       * @return  {@code true} if the provided password is equal to any of the
3719       *          current passwords or any of the passwords in the history, or
3720       *          {@code false} if not.
3721       */
3722      public boolean isPasswordInHistory(ByteString password)
3723      {
3724        if (! maintainHistory())
3725        {
3726          if (debugEnabled())
3727          {
3728            TRACER.debugInfo("Returning false because password history " +
3729                "checking is disabled.");
3730          }
3731    
3732          // Password history checking is disabled, so we don't care if it is in the
3733          // list or not.
3734          return false;
3735        }
3736    
3737    
3738        // Check to see if the provided password is equal to any of the current
3739        // passwords.  If so, then we'll consider it to be in the history.
3740        if (passwordMatches(password))
3741        {
3742          if (debugEnabled())
3743          {
3744            TRACER.debugInfo("Returning true because the provided password " +
3745                "is currently in use.");
3746          }
3747    
3748          return true;
3749        }
3750    
3751    
3752        // Get the attribute containing the history and check to see if any of the
3753        // values is equal to the provided password.  However, first prune the list
3754        // by size and duration if necessary.
3755        TreeMap<Long,AttributeValue> historyMap = getSortedHistoryValues(null);
3756    
3757        int historyCount = passwordPolicy.getPasswordHistoryCount();
3758        if ((historyCount > 0) && (historyMap.size() > historyCount))
3759        {
3760          int numToDelete = historyMap.size() - historyCount;
3761          Iterator<Long> iterator = historyMap.keySet().iterator();
3762          while ((iterator.hasNext()) && (numToDelete > 0))
3763          {
3764            iterator.next();
3765            iterator.remove();
3766            numToDelete--;
3767          }
3768        }
3769    
3770        int historyDuration = passwordPolicy.getPasswordHistoryDuration();
3771        if (historyDuration > 0)
3772        {
3773          long retainDate = currentTime - (1000 * historyDuration);
3774          Iterator<Long> iterator = historyMap.keySet().iterator();
3775          while (iterator.hasNext())
3776          {
3777            long historyDate = iterator.next();
3778            if (historyDate < retainDate)
3779            {
3780              iterator.remove();
3781            }
3782            else
3783            {
3784              break;
3785            }
3786          }
3787        }
3788    
3789        for (AttributeValue v : historyMap.values())
3790        {
3791          if (historyValueMatches(password, v))
3792          {
3793            if (debugEnabled())
3794            {
3795              TRACER.debugInfo("Returning true because the password is in " +
3796                  "the history.");
3797            }
3798    
3799            return true;
3800          }
3801        }
3802    
3803    
3804        // If we've gotten here, then the password isn't in the history.
3805        if (debugEnabled())
3806        {
3807          TRACER.debugInfo("Returning false because the password isn't in the " +
3808              "history.");
3809        }
3810    
3811        return false;
3812      }
3813    
3814    
3815    
3816      /**
3817       * Gets a sorted list of the password history values contained in the user's
3818       * entry.  The values will be sorted by timestamp.
3819       *
3820       * @param  removeAttrs  A list into which any values will be placed that could
3821       *                      not be properly decoded.  It may be {@code null} if
3822       *                      this is not needed.
3823       */
3824      private TreeMap<Long,AttributeValue> getSortedHistoryValues(List<Attribute>
3825                                                                       removeAttrs)
3826      {
3827        TreeMap<Long,AttributeValue> historyMap =
3828             new TreeMap<Long,AttributeValue>();
3829        AttributeType historyType =
3830             DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_HISTORY_LC, true);
3831        List<Attribute> attrList = userEntry.getAttribute(historyType);
3832        if (attrList != null)
3833        {
3834          for (Attribute a : attrList)
3835          {
3836            for (AttributeValue v : a.getValues())
3837            {
3838              String histStr = v.getStringValue();
3839              int    hashPos = histStr.indexOf('#');
3840              if (hashPos <= 0)
3841              {
3842                if (debugEnabled())
3843                {
3844                  TRACER.debugInfo("Found value " + histStr + " in the " +
3845                      "history with no timestamp.  Marking it " +
3846                      "for removal.");
3847                }
3848    
3849                LinkedHashSet<AttributeValue> values =
3850                     new LinkedHashSet<AttributeValue>(1);
3851                values.add(v);
3852                if (removeAttrs != null)
3853                {
3854                  removeAttrs.add(new Attribute(a.getAttributeType(), a.getName(),
3855                                                values));
3856                }
3857              }
3858              else
3859              {
3860                try
3861                {
3862                  long timestamp =
3863                       GeneralizedTimeSyntax.decodeGeneralizedTimeValue(
3864                            new ASN1OctetString(histStr.substring(0, hashPos)));
3865                  historyMap.put(timestamp, v);
3866                }
3867                catch (Exception e)
3868                {
3869                  if (debugEnabled())
3870                  {
3871                    TRACER.debugCaught(DebugLogLevel.ERROR, e);
3872    
3873                    TRACER.debugInfo("Could not decode the timestamp in " +
3874                        "history value " + histStr + " -- " + e +
3875                        ".  Marking it for removal.");
3876                  }
3877    
3878                  LinkedHashSet<AttributeValue> values =
3879                       new LinkedHashSet<AttributeValue>(1);
3880                  values.add(v);
3881                  if (removeAttrs != null)
3882                  {
3883                    removeAttrs.add(new Attribute(a.getAttributeType(), a.getName(),
3884                                                  values));
3885                  }
3886                }
3887              }
3888            }
3889          }
3890        }
3891    
3892        return historyMap;
3893      }
3894    
3895    
3896    
3897      /**
3898       * Indicates whether the provided password matches the given history value.
3899       *
3900       * @param  password      The clear-text password for which to make the
3901       *                       determination.
3902       * @param  historyValue  The encoded history value to compare against the
3903       *                       clear-text password.
3904       *
3905       * @return  {@code true} if the provided password matches the history value,
3906       *          or {@code false} if not.
3907       */
3908      private boolean historyValueMatches(ByteString password,
3909                                          AttributeValue historyValue)
3910      {
3911        // According to draft-behera-ldap-password-policy, password history values
3912        // should be in the format time#syntaxoid#encodedvalue.  In this method,
3913        // we only care about the syntax OID and encoded password.
3914        try
3915        {
3916          String histStr  = historyValue.getStringValue();
3917          int    hashPos1 = histStr.indexOf('#');
3918          if (hashPos1 <= 0)
3919          {
3920            if (debugEnabled())
3921            {
3922              TRACER.debugInfo("Returning false because the password history " +
3923                  "value didn't include any hash characters.");
3924            }
3925    
3926            return false;
3927          }
3928    
3929          int hashPos2 = histStr.indexOf('#', hashPos1+1);
3930          if (hashPos2 < 0)
3931          {
3932            if (debugEnabled())
3933            {
3934              TRACER.debugInfo("Returning false because the password history " +
3935                  "value only had one hash character.");
3936            }
3937    
3938            return false;
3939          }
3940    
3941          String syntaxOID = toLowerCase(histStr.substring(hashPos1+1, hashPos2));
3942          if (syntaxOID.equals(SYNTAX_AUTH_PASSWORD_OID))
3943          {
3944            StringBuilder[] authPWComponents =
3945                 AuthPasswordSyntax.decodeAuthPassword(
3946                      histStr.substring(hashPos2+1));
3947            PasswordStorageScheme scheme =
3948                 DirectoryServer.getAuthPasswordStorageScheme(
3949                      authPWComponents[0].toString());
3950            if (scheme.authPasswordMatches(password, authPWComponents[1].toString(),
3951                                           authPWComponents[2].toString()))
3952            {
3953              if (debugEnabled())
3954              {
3955                TRACER.debugInfo("Returning true because the auth password " +
3956                    "history value matched.");
3957              }
3958    
3959              return true;
3960            }
3961            else
3962            {
3963              if (debugEnabled())
3964              {
3965                TRACER.debugInfo("Returning false because the auth password " +
3966                    "history value did not match.");
3967              }
3968    
3969              return false;
3970            }
3971          }
3972          else if (syntaxOID.equals(SYNTAX_USER_PASSWORD_OID))
3973          {
3974            String[] userPWComponents =
3975                 UserPasswordSyntax.decodeUserPassword(
3976                      histStr.substring(hashPos2+1));
3977            PasswordStorageScheme scheme =
3978                 DirectoryServer.getPasswordStorageScheme(userPWComponents[0]);
3979            if (scheme.passwordMatches(password,
3980                                       new ASN1OctetString(userPWComponents[1])))
3981            {
3982              if (debugEnabled())
3983              {
3984                TRACER.debugInfo("Returning true because the user password " +
3985                    "history value matched.");
3986              }
3987    
3988              return true;
3989            }
3990            else
3991            {
3992              if (debugEnabled())
3993              {
3994                TRACER.debugInfo("Returning false because the user password " +
3995                    "history value did not match.");
3996              }
3997    
3998              return false;
3999            }
4000          }
4001          else
4002          {
4003            if (debugEnabled())
4004            {
4005              TRACER.debugInfo("Returning false because the syntax OID " +
4006                  syntaxOID + " didn't match for either the auth " +
4007                  "or user password syntax.");
4008            }
4009    
4010            return false;
4011          }
4012        }
4013        catch (Exception e)
4014        {
4015          if (debugEnabled())
4016          {
4017            TRACER.debugCaught(DebugLogLevel.ERROR, e);
4018    
4019            if (debugEnabled())
4020            {
4021              TRACER.debugInfo("Returning false because of an exception:  " +
4022                               stackTraceToSingleLineString(e));
4023            }
4024          }
4025    
4026          return false;
4027        }
4028      }
4029    
4030    
4031    
4032      /**
4033       * Updates the password history information for this user by adding all
4034       * current passwords to it.
4035       */
4036      public void updatePasswordHistory()
4037      {
4038        List<Attribute> attrList =
4039             userEntry.getAttribute(passwordPolicy.getPasswordAttribute());
4040        if (attrList != null)
4041        {
4042          for (Attribute a : attrList)
4043          {
4044            for (AttributeValue v : a.getValues())
4045            {
4046              addPasswordToHistory(v.getStringValue());
4047            }
4048          }
4049        }
4050      }
4051    
4052    
4053    
4054      /**
4055       * Adds the provided password to the password history.  If appropriate, one or
4056       * more old passwords may be evicted from the list if the total size would
4057       * exceed the configured count, or if passwords are older than the configured
4058       * duration.
4059       *
4060       * @param  encodedPassword  The encoded password (in either user password or
4061       *                          auth password format) to be added to the history.
4062       */
4063      private void addPasswordToHistory(String encodedPassword)
4064      {
4065        if (! maintainHistory())
4066        {
4067          if (debugEnabled())
4068          {
4069            TRACER.debugInfo("Not doing anything because password history " +
4070                "maintenance is disabled.");
4071          }
4072    
4073          return;
4074        }
4075    
4076    
4077        // Get a sorted list of the existing values to see if there are any that
4078        // should be removed.
4079        LinkedList<Attribute> removeAttrs = new LinkedList<Attribute>();
4080        TreeMap<Long,AttributeValue> historyMap =
4081             getSortedHistoryValues(removeAttrs);
4082    
4083    
4084        // If there is a maximum number of values to retain and we would be over the
4085        // limit with the new value, then get rid of enough values (oldest first)
4086        // to satisfy the count.
4087        AttributeType historyType =
4088             DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_HISTORY_LC, true);
4089        int historyCount = passwordPolicy.getPasswordHistoryCount();
4090        if  ((historyCount > 0) && (historyMap.size() >= historyCount))
4091        {
4092          int numToDelete = (historyMap.size() - historyCount) + 1;
4093          LinkedHashSet<AttributeValue> removeValues =
4094               new LinkedHashSet<AttributeValue>(numToDelete);
4095          Iterator<AttributeValue> iterator = historyMap.values().iterator();
4096          while (iterator.hasNext() && (numToDelete > 0))
4097          {
4098            AttributeValue v = iterator.next();
4099            removeValues.add(v);
4100            iterator.remove();
4101            numToDelete--;
4102    
4103            if (debugEnabled())
4104            {
4105              TRACER.debugInfo("Removing history value " + v.getStringValue() +
4106                  " to preserve the history count.");
4107            }
4108          }
4109    
4110          if (! removeValues.isEmpty())
4111          {
4112            removeAttrs.add(new Attribute(historyType, historyType.getPrimaryName(),
4113                                          removeValues));
4114          }
4115        }
4116    
4117    
4118        // If there is a maximum duration, then get rid of any values that would be
4119        // over the duration.
4120        int historyDuration = passwordPolicy.getPasswordHistoryDuration();
4121        if (historyDuration > 0)
4122        {
4123          long minAgeToKeep = currentTime - (1000L * historyDuration);
4124          Iterator<Long> iterator = historyMap.keySet().iterator();
4125          LinkedHashSet<AttributeValue> removeValues =
4126               new LinkedHashSet<AttributeValue>();
4127          while (iterator.hasNext())
4128          {
4129            long timestamp = iterator.next();
4130            if (timestamp < minAgeToKeep)
4131            {
4132              AttributeValue v = historyMap.get(timestamp);
4133              removeValues.add(v);
4134              iterator.remove();
4135    
4136              if (debugEnabled())
4137              {
4138                TRACER.debugInfo("Removing history value " + v.getStringValue() +
4139                    " to preserve the history duration.");
4140              }
4141            }
4142            else
4143            {
4144              break;
4145            }
4146          }
4147    
4148          if (! removeValues.isEmpty())
4149          {
4150            removeAttrs.add(new Attribute(historyType, historyType.getPrimaryName(),
4151                                          removeValues));
4152          }
4153        }
4154    
4155    
4156        // At this point, we can add the new value.  However, we want to make sure
4157        // that its timestamp (which is the current time) doesn't conflict with any
4158        // value already in the list.  If there is a conflict, then simply add one
4159        // to it until we don't have any more conflicts.
4160        long newTimestamp = currentTime;
4161        while (historyMap.containsKey(newTimestamp))
4162        {
4163          newTimestamp++;
4164        }
4165        String newHistStr = GeneralizedTimeSyntax.format(newTimestamp) + "#" +
4166                            passwordPolicy.getPasswordAttribute().getSyntaxOID() +
4167                            "#" + encodedPassword;
4168        LinkedHashSet<AttributeValue> newHistValues =
4169             new LinkedHashSet<AttributeValue>(1);
4170        newHistValues.add(new AttributeValue(historyType, newHistStr));
4171        Attribute newHistAttr =
4172             new Attribute(historyType, historyType.getPrimaryName(),
4173                           newHistValues);
4174    
4175        if (debugEnabled())
4176        {
4177          TRACER.debugInfo("Going to add history value " + newHistStr);
4178        }
4179    
4180    
4181        // Apply the changes, either by adding modifications or by directly updating
4182        // the entry.
4183        if (updateEntry)
4184        {
4185          LinkedList<AttributeValue> valueList = new LinkedList<AttributeValue>();
4186          for (Attribute a : removeAttrs)
4187          {
4188            userEntry.removeAttribute(a, valueList);
4189          }
4190    
4191          userEntry.addAttribute(newHistAttr, valueList);
4192        }
4193        else
4194        {
4195          for (Attribute a : removeAttrs)
4196          {
4197            modifications.add(new Modification(ModificationType.DELETE, a, true));
4198          }
4199    
4200          modifications.add(new Modification(ModificationType.ADD, newHistAttr,
4201                                             true));
4202        }
4203      }
4204    
4205    
4206    
4207      /**
4208       * Retrieves the password history state values for the user.  This is only
4209       * intended for testing purposes.
4210       *
4211       * @return  The password history state values for the user.
4212       */
4213      public String[] getPasswordHistoryValues()
4214      {
4215        ArrayList<String> historyValues = new ArrayList<String>();
4216        AttributeType historyType =
4217             DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_HISTORY_LC, true);
4218        List<Attribute> attrList = userEntry.getAttribute(historyType);
4219        if (attrList != null)
4220        {
4221          for (Attribute a : attrList)
4222          {
4223            for (AttributeValue v : a.getValues())
4224            {
4225              historyValues.add(v.getStringValue());
4226            }
4227          }
4228        }
4229    
4230        String[] historyArray = new String[historyValues.size()];
4231        return historyValues.toArray(historyArray);
4232      }
4233    
4234    
4235    
4236      /**
4237       * Clears the password history state information for the user.  This is only
4238       * intended for testing purposes.
4239       */
4240      public void clearPasswordHistory()
4241      {
4242        if (debugEnabled())
4243        {
4244          TRACER.debugInfo("Clearing password history for user %s", userDNString);
4245        }
4246    
4247        AttributeType type = DirectoryServer.getAttributeType(
4248                                 OP_ATTR_PWPOLICY_HISTORY_LC, true);
4249        if (updateEntry)
4250        {
4251          userEntry.removeAttribute(type);
4252        }
4253        else
4254        {
4255          modifications.add(new Modification(ModificationType.REPLACE,
4256                                             new Attribute(type), true));
4257        }
4258      }
4259    
4260    
4261    
4262      /**
4263       * Generates a new password for the user.
4264       *
4265       * @return  The new password that has been generated, or <CODE>null</CODE> if
4266       *          no password generator has been defined.
4267       *
4268       * @throws  DirectoryException  If an error occurs while attempting to
4269       *                              generate the new password.
4270       */
4271      public ByteString generatePassword()
4272          throws DirectoryException
4273      {
4274        PasswordGenerator generator = passwordPolicy.getPasswordGenerator();
4275        if (generator == null)
4276        {
4277          if (debugEnabled())
4278          {
4279            TRACER.debugWarning("Unable to generate a new password for user " +
4280                "%s because no password generator has been defined in the " +
4281                "associated password policy.", userDNString);
4282          }
4283    
4284          return null;
4285        }
4286    
4287        return generator.generatePassword(userEntry);
4288      }
4289    
4290    
4291    
4292      /**
4293       * Generates an account status notification for this user.
4294       *
4295       * @param  notificationType        The type for the account status
4296       *                                 notification.
4297       * @param  userEntry               The entry for the user to which this
4298       *                                 notification applies.
4299       * @param  message                 The human-readable message for the
4300       *                                 notification.
4301       * @param  notificationProperties  The set of properties for the notification.
4302       */
4303      public void generateAccountStatusNotification(
4304              AccountStatusNotificationType notificationType,
4305              Entry userEntry, Message message,
4306              Map<AccountStatusNotificationProperty,List<String>>
4307                   notificationProperties)
4308      {
4309        generateAccountStatusNotification(new AccountStatusNotification(
4310             notificationType, userEntry, message, notificationProperties));
4311      }
4312    
4313    
4314    
4315      /**
4316       * Generates an account status notification for this user.
4317       *
4318       * @param  notification  The account status notification that should be
4319       *                       generated.
4320       */
4321      public void generateAccountStatusNotification(
4322                       AccountStatusNotification notification)
4323      {
4324        Collection<AccountStatusNotificationHandler> handlers =
4325             passwordPolicy.getAccountStatusNotificationHandlers().values();
4326        if ((handlers == null) || handlers.isEmpty())
4327        {
4328          return;
4329        }
4330    
4331        for (AccountStatusNotificationHandler handler : handlers)
4332        {
4333          handler.handleStatusNotification(notification);
4334        }
4335      }
4336    
4337    
4338    
4339      /**
4340       * Retrieves the set of modifications that correspond to changes made in
4341       * password policy processing that may need to be applied to the user entry.
4342       *
4343       * @return  The set of modifications that correspond to changes made in
4344       *          password policy processing that may need to be applied to the user
4345       *          entry.
4346       */
4347      public LinkedList<Modification> getModifications()
4348      {
4349        return modifications;
4350      }
4351    
4352    
4353    
4354      /**
4355       * Performs an internal modification to update the user's entry, if necessary.
4356       * This will do nothing if no modifications are required.
4357       *
4358       * @throws  DirectoryException  If a problem occurs while processing the
4359       *                              internal modification.
4360       */
4361      public void updateUserEntry()
4362             throws DirectoryException
4363      {
4364        // If there are no modifications, then there's nothing to do.
4365        if (modifications.isEmpty())
4366        {
4367          return;
4368        }
4369    
4370    
4371        // Convert the set of modifications to a set of LDAP modifications.
4372        ArrayList<RawModification> modList = new ArrayList<RawModification>();
4373        for (Modification m : modifications)
4374        {
4375          modList.add(RawModification.create(m.getModificationType(),
4376                           new LDAPAttribute(m.getAttribute())));
4377        }
4378    
4379        InternalClientConnection conn =
4380             InternalClientConnection.getRootConnection();
4381        ModifyOperation internalModify =
4382             conn.processModify(new ASN1OctetString(userDNString), modList);
4383    
4384        ResultCode resultCode = internalModify.getResultCode();
4385        if (resultCode != ResultCode.SUCCESS)
4386        {
4387          Message message = ERR_PWPSTATE_CANNOT_UPDATE_USER_ENTRY.get(userDNString,
4388                                String.valueOf(internalModify.getErrorMessage()));
4389    
4390          // If this is a root user, or if the password policy says that we should
4391          // ignore these problems, then log a warning message.  Otherwise, cause
4392          // the bind to fail.
4393          if ((DirectoryServer.isRootDN(userEntry.getDN()) ||
4394              (passwordPolicy.getStateUpdateFailurePolicy() ==
4395               PasswordPolicyCfgDefn.StateUpdateFailurePolicy.IGNORE)))
4396          {
4397            ErrorLogger.logError(message);
4398          }
4399          else
4400          {
4401            throw new DirectoryException(resultCode, message);
4402          }
4403        }
4404      }
4405    }
4406