/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at legal-notices/CDDLv1_0.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2006-2010 Sun Microsystems, Inc. * Portions Copyright 2011-2015 ForgeRock AS */ package org.opends.server.core; import static org.opends.messages.CoreMessages.*; import static org.opends.server.config.ConfigConstants.*; import static org.opends.server.protocols.internal.InternalClientConnection.*; import static org.opends.server.schema.SchemaConstants.*; import static org.opends.server.util.StaticUtils.*; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TimeZone; import java.util.TreeMap; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.LocalizableMessageBuilder; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.ldap.ByteString; import org.forgerock.opendj.ldap.ConditionResult; import org.forgerock.opendj.ldap.GeneralizedTime; import org.forgerock.opendj.ldap.ModificationType; import org.forgerock.opendj.ldap.ResultCode; import org.opends.server.admin.std.meta.PasswordPolicyCfgDefn; import org.opends.server.api.AccountStatusNotificationHandler; import org.opends.server.api.AuthenticationPolicyState; import org.opends.server.api.PasswordGenerator; import org.opends.server.api.PasswordStorageScheme; import org.opends.server.api.PasswordValidator; import org.opends.server.protocols.internal.InternalClientConnection; import org.opends.server.protocols.ldap.LDAPAttribute; import org.opends.server.schema.AuthPasswordSyntax; import org.opends.server.schema.GeneralizedTimeSyntax; import org.opends.server.schema.UserPasswordSyntax; import org.opends.server.types.AccountStatusNotification; import org.opends.server.types.AccountStatusNotificationProperty; import org.opends.server.types.AccountStatusNotificationType; import org.opends.server.types.Attribute; import org.opends.server.types.AttributeBuilder; import org.opends.server.types.AttributeType; import org.opends.server.types.Attributes; import org.opends.server.types.DirectoryException; import org.opends.server.types.Entry; import org.opends.server.types.Modification; import org.opends.server.types.Operation; import org.opends.server.types.RawModification; /** * This class provides a data structure for holding password policy state * information for a user account. */ public final class PasswordPolicyState extends AuthenticationPolicyState { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); /** The string representation of the user's DN. */ private final String userDNString; /** The password policy with which the account is associated. */ private final PasswordPolicy passwordPolicy; /** The current time for use in all password policy calculations. */ private final long currentTime; /** The time that the user's password was last changed. */ private long passwordChangedTime = Long.MIN_VALUE; /** Indicates whether the user's account is expired. */ private ConditionResult isAccountExpired = ConditionResult.UNDEFINED; /** Indicates whether the user's password is expired. */ private ConditionResult isPasswordExpired = ConditionResult.UNDEFINED; /** Indicates whether the warning to send to the client would be the first warning for the user. */ private ConditionResult isFirstWarning = ConditionResult.UNDEFINED; /** Indicates whether the user's account is locked by the idle lockout. */ private ConditionResult isIdleLocked = ConditionResult.UNDEFINED; /** * Indicates whether the user may use a grace login if the password is expired and there are one * or more grace logins remaining. */ private ConditionResult mayUseGraceLogin = ConditionResult.UNDEFINED; /** Indicates whether the user's password must be changed. */ private ConditionResult mustChangePassword = ConditionResult.UNDEFINED; /** Indicates whether the user should be warned of an upcoming expiration. */ private ConditionResult shouldWarn = ConditionResult.UNDEFINED; /** The number of seconds until the user's account is automatically unlocked. */ private int secondsUntilUnlock = Integer.MIN_VALUE; /** The set of authentication failure times for this user. */ private List<Long> authFailureTimes; /** The set of grace login times for this user. */ private List<Long> graceLoginTimes; /** The time that the user's account should expire (or did expire). */ private long accountExpirationTime = Long.MIN_VALUE; /** The time that the user's entry was locked due to too many authentication failures. */ private long failureLockedTime = Long.MIN_VALUE; /** The time that the user last authenticated to the Directory Server. */ private long lastLoginTime = Long.MIN_VALUE; /** The time that the user's password should expire (or did expire). */ private long passwordExpirationTime = Long.MIN_VALUE; /** The last required change time with which the user complied. */ private long requiredChangeTime = Long.MIN_VALUE; /** The time that the user was first warned about an upcoming expiration. */ private long warnedTime = Long.MIN_VALUE; /** The set of modifications that should be applied to the user's entry. */ private LinkedList<Modification> modifications = new LinkedList<>(); /** * Creates a new password policy state object with the provided information. * <p> * Note that this version of the constructor should only be used for testing purposes when the tests should be * evaluated with a fixed time rather than the actual current time. For all other purposes, the other constructor * should be used. * </p> * * @param policy The password policy associated with the state. * @param userEntry The entry with the user account. * @param currentTime The time to use as the current time for all time-related determinations. */ PasswordPolicyState(PasswordPolicy policy, Entry userEntry, long currentTime) { super(userEntry); this.currentTime = currentTime; this.userDNString = userEntry.getName().toString(); this.passwordPolicy = policy; } /** * Retrieves the value of the specified attribute as a string. * * @param attributeType The attribute type whose value should be retrieved. * * @return The value of the specified attribute as a string, or <CODE>null</CODE> if there is no such value. */ private String getValue(AttributeType attributeType) { Attribute attr = getFirstAttributeNotEmpty(attributeType); String stringValue = attr != null ? attr.iterator().next().toString() : null; if (stringValue == null) { if (logger.isTraceEnabled()) { logger.trace("Returning null because attribute %s does not exist in user entry %s", attributeType.getNameOrOID(), userDNString); } } else { if (logger.isTraceEnabled()) { logger.trace("Returning value %s for user %s", stringValue, userDNString); } } return stringValue; } private Attribute getFirstAttributeNotEmpty(AttributeType attributeType) { List<Attribute> attrList = userEntry.getAttribute(attributeType); if (attrList != null) { for (Attribute a : attrList) { if (!a.isEmpty()) { return a; } } } return null; } /** * Retrieves the set of values of the specified attribute from the user's entry in generalized time format. * * @param attributeType The attribute type whose values should be parsed as generalized time values. * * @return The set of generalized time values, or an empty list if there are none. * * @throws DirectoryException If a problem occurs while attempting to decode a value as a generalized time. */ private List<Long> getGeneralizedTimes(AttributeType attributeType) throws DirectoryException { ArrayList<Long> timeValues = new ArrayList<>(); List<Attribute> attrList = userEntry.getAttribute(attributeType); if (attrList != null) { for (Attribute a : attrList) { for (ByteString v : a) { try { timeValues.add(GeneralizedTime.valueOf(v.toString()).getTimeInMillis()); } catch (Exception e) { logger.traceException(e, "Unable to decode value %s for attribute %s in user entry %s", v, attributeType.getNameOrOID(), userDNString); throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, ERR_PWPSTATE_CANNOT_DECODE_GENERALIZED_TIME.get(v, attributeType.getNameOrOID(), userDNString, e), e); } } } } if (timeValues.isEmpty()) { logger.trace("Returning an empty list because attribute %s does not exist in user entry %s", attributeType.getNameOrOID(), userDNString); } return timeValues; } /** * Get the password storage scheme used by a given password value. * * @param v The encoded password value to check. * * @return The scheme used by the password. * * @throws DirectoryException If the password could not be decoded. */ private PasswordStorageScheme<?> getPasswordStorageScheme(ByteString v) throws DirectoryException { if (passwordPolicy.isAuthPasswordSyntax()) { String[] pwComps = AuthPasswordSyntax.decodeAuthPassword(v.toString()); return DirectoryServer.getAuthPasswordStorageScheme(pwComps[0]); } else { String[] pwComps = UserPasswordSyntax.decodeUserPassword(v.toString()); return DirectoryServer.getPasswordStorageScheme(pwComps[0]); } } @Override public PasswordPolicy getAuthenticationPolicy() { return passwordPolicy; } /** * Retrieves the time that the password was last changed. * * @return The time that the password was last changed. */ public long getPasswordChangedTime() { if (passwordChangedTime < 0) { // Get the password changed time for the user. try { passwordChangedTime = getGeneralizedTime0(userEntry, OP_ATTR_PWPOLICY_CHANGED_TIME_LC); } catch (DirectoryException e) { /* * The password change time could not be parsed (but has been logged in the debug log). * The best effort we can do from here is to a) use the current time, b) use the start * of the epoch (1/1/1970), or c) use the create time stamp. Lets treat this problem as if the change time * attribute did not exist and resort to the create time stamp. */ } if (passwordChangedTime < 0) { // Get the time that the user's account was created. try { passwordChangedTime = getGeneralizedTime0(userEntry, OP_ATTR_CREATE_TIMESTAMP_LC); } catch (DirectoryException e) { /* * The create time stamp could not be parsed (but has been logged in the debug log). * The best effort we can do from here is to a) use the current time, or b) use the start of * the epoch (1/1/1970). Lets treat this problem as if the change time attribute did not exist * and use the start of the epoch. Doing so stands a greater chance of forcing a password change. */ } if (passwordChangedTime < 0) { passwordChangedTime = 0; if (logger.isTraceEnabled()) { logger.trace( "Could not determine password changed time for " + "user %s.", userDNString); } } } } return passwordChangedTime; } private long getGeneralizedTime0(Entry userEntry, String attrName) throws DirectoryException { return getGeneralizedTime(userEntry, DirectoryServer.getAttributeTypeOrDefault(attrName)); } /** * Retrieves the time that this password policy state object was created. * * @return The time that this password policy state object was created. */ public long getCurrentTime() { return currentTime; } /** * Retrieves the unmodifiable set of values for the password attribute from the user entry. * * @return The unmodifiable set of values for the password attribute from the user entry. */ public Set<ByteString> getPasswordValues() { final Attribute attr = getFirstAttributeNotEmpty(passwordPolicy.getPasswordAttribute()); if (attr != null) { Set<ByteString> values = new LinkedHashSet<>(attr.size()); for (ByteString value : attr) { values.add(value); } return Collections.unmodifiableSet(values); } return Collections.emptySet(); } /** * Sets a new value for the password changed time equal to the current time. */ public void setPasswordChangedTime() { setPasswordChangedTime(currentTime); } /** * Sets a new value for the password changed time equal to the specified time. * This method should generally only be used for testing purposes, since the variant that uses * the current time is preferred almost everywhere else. * * @param passwordChangedTime The time to use */ public void setPasswordChangedTime(long passwordChangedTime) { if (logger.isTraceEnabled()) { logger.trace("Setting password changed time for user %s to current time of %d", userDNString, currentTime); } // passwordChangedTime is computed in the constructor from values in the entry. if (getPasswordChangedTime() != passwordChangedTime) { this.passwordChangedTime = passwordChangedTime; String timeValue = GeneralizedTimeSyntax.format(passwordChangedTime); Attribute a = Attributes.create(OP_ATTR_PWPOLICY_CHANGED_TIME, timeValue); modifications.add(new Modification(ModificationType.REPLACE, a, true)); } } /** * Removes the password changed time value from the user's entry. This should only be used for testing * purposes, as it can really mess things up if you don't know what you're doing. */ public void clearPasswordChangedTime() { if (logger.isTraceEnabled()) { logger.trace("Clearing password changed time for user %s", userDNString); } Attribute a = Attributes.empty(OP_ATTR_PWPOLICY_CHANGED_TIME_LC); modifications.add(new Modification(ModificationType.REPLACE, a, true)); // Fall back to using the entry creation time as the password changed time, if it's defined. // Otherwise, use a value of zero. try { passwordChangedTime = getGeneralizedTime0(userEntry, OP_ATTR_CREATE_TIMESTAMP_LC); if (passwordChangedTime < 0) { passwordChangedTime = 0; } } catch (Exception e) { passwordChangedTime = 0; } } /** * Updates the user entry to indicate whether user account has been administratively disabled. * * @param isDisabled * Indicates whether the user account has been administratively disabled. */ public void setDisabled(boolean isDisabled) { if (logger.isTraceEnabled()) { logger.trace("Updating user %s to set the disabled flag to %b", userDNString, isDisabled); } if (isDisabled == isDisabled()) { return; // requested state matches current state } this.isDisabled = ConditionResult.not(this.isDisabled); if (isDisabled) { Attribute a = Attributes.create(OP_ATTR_ACCOUNT_DISABLED, String.valueOf(true)); modifications.add(new Modification(ModificationType.REPLACE, a, true)); } else { // erase modifications.add(new Modification(ModificationType.REPLACE, Attributes.empty(OP_ATTR_ACCOUNT_DISABLED), true)); } } /** * Indicates whether the user's account is currently expired. * * @return <CODE>true</CODE> if the user's account is expired, or <CODE>false</CODE> if not. */ public boolean isAccountExpired() { if (isAccountExpired != ConditionResult.UNDEFINED) { if (logger.isTraceEnabled()) { logger.trace("Returning stored result of %b for user %s", isAccountExpired == ConditionResult.TRUE, userDNString); } return isAccountExpired == ConditionResult.TRUE; } try { accountExpirationTime = getGeneralizedTime0(userEntry, OP_ATTR_ACCOUNT_EXPIRATION_TIME); } catch (Exception e) { logger.traceException(e, "User %s is considered to have an expired account because an error occurred " + "while attempting to make the determination.", userDNString); isAccountExpired = ConditionResult.TRUE; return true; } if (accountExpirationTime > currentTime) { // The user does have an expiration time, but it hasn't arrived yet. isAccountExpired = ConditionResult.FALSE; logger.trace("The account for user %s is not expired because the expiration time has not yet arrived.", userDNString); } else if (accountExpirationTime >= 0) { // The user does have an expiration time, and it is in the past. isAccountExpired = ConditionResult.TRUE; logger.trace("The account for user %s is expired because the expiration time in that account has passed.", userDNString); } else { // The user doesn't have an expiration time in their entry, so it can't be expired. isAccountExpired = ConditionResult.FALSE; logger.trace("The account for user %s is not expired because there is no expiration time in the user's entry.", userDNString); } return isAccountExpired == ConditionResult.TRUE; } /** * Retrieves the time at which the user's account will expire. * * @return The time at which the user's account will expire, or -1 if it is not configured with an expiration time. */ public long getAccountExpirationTime() { if (accountExpirationTime == Long.MIN_VALUE) { isAccountExpired(); } return accountExpirationTime; } /** * Sets the user's account expiration time to the specified value. * * @param accountExpirationTime The time that the user's account should expire. */ public void setAccountExpirationTime(long accountExpirationTime) { if (accountExpirationTime < 0) { clearAccountExpirationTime(); } else { String timeStr = GeneralizedTimeSyntax.format(accountExpirationTime); if (logger.isTraceEnabled()) { logger.trace("Setting account expiration time for user %s to %s", userDNString, timeStr); } this.accountExpirationTime = accountExpirationTime; Attribute a = Attributes.create(OP_ATTR_ACCOUNT_EXPIRATION_TIME, timeStr); modifications.add(new Modification(ModificationType.REPLACE, a, true)); } } /** * Clears the user's account expiration time. */ public void clearAccountExpirationTime() { if (logger.isTraceEnabled()) { logger.trace("Clearing account expiration time for user %s", userDNString); } accountExpirationTime = -1; String attrName = OP_ATTR_ACCOUNT_EXPIRATION_TIME; modifications.add(new Modification(ModificationType.REPLACE, Attributes.empty(attrName), true)); } /** * Retrieves the set of times of failed authentication attempts for the user. If authentication failure * time expiration is enabled, and there are expired times in the entry, these times are removed * from the instance field and an update is provided to delete those values from the entry. * * @return The set of times of failed authentication attempts for the user, which will be an empty list * in the case of no valid (unexpired) times in the entry. */ public List<Long> getAuthFailureTimes() { if (authFailureTimes != null) { if (logger.isTraceEnabled()) { logger.trace("Returning stored auth failure time list of %d elements for user %s", authFailureTimes.size(), userDNString); } return authFailureTimes; } AttributeType type = DirectoryServer.getAttributeTypeOrDefault( OP_ATTR_PWPOLICY_FAILURE_TIME_LC, OP_ATTR_PWPOLICY_FAILURE_TIME); try { authFailureTimes = getGeneralizedTimes(type); } catch (Exception e) { logger.traceException(e, "Error while processing auth failure times for user %s", userDNString); authFailureTimes = new ArrayList<>(); modifications.add(new Modification(ModificationType.REPLACE, Attributes.empty(type), true)); return authFailureTimes; } if (authFailureTimes.isEmpty()) { if (logger.isTraceEnabled()) { logger.trace("Returning an empty auth failure time list for user %s because the attribute" + " is absent from the entry.", userDNString); } return authFailureTimes; } // Remove any expired failures from the list. if (passwordPolicy.getLockoutFailureExpirationInterval() > 0) { LinkedHashSet<ByteString> valuesToRemove = null; long expirationTime = currentTime - passwordPolicy.getLockoutFailureExpirationInterval() * 1000L; Iterator<Long> iterator = authFailureTimes.iterator(); while (iterator.hasNext()) { long l = iterator.next(); if (l < expirationTime) { if (logger.isTraceEnabled()) { logger.trace("Removing expired auth failure time %d for user %s", l, userDNString); } iterator.remove(); if (valuesToRemove == null) { valuesToRemove = new LinkedHashSet<>(); } valuesToRemove.add(ByteString.valueOfUtf8(GeneralizedTimeSyntax.format(l))); } } if (valuesToRemove != null) { Attribute a = newAttribute(type, valuesToRemove); modifications.add(new Modification(ModificationType.DELETE, a, true)); } } if (logger.isTraceEnabled()) { logger.trace("Returning auth failure time list of %d elements for user %s", authFailureTimes.size(), userDNString); } return authFailureTimes; } /** * Updates the set of authentication failure times to include the current time. * If the number of failures reaches the policy configuration limit, lock the account. */ public void updateAuthFailureTimes() { if (passwordPolicy.getLockoutFailureCount() <= 0) { return; } if (logger.isTraceEnabled()) { logger.trace("Updating authentication failure times for user %s", userDNString); } List<Long> failureTimes = getAuthFailureTimes(); long highestFailureTime = computeHighestTime(failureTimes); // Update the current policy state failureTimes.add(highestFailureTime); // And the attribute in the user entry AttributeType type = DirectoryServer.getAttributeTypeOrDefault( OP_ATTR_PWPOLICY_FAILURE_TIME_LC, OP_ATTR_PWPOLICY_FAILURE_TIME); Attribute addAttr = Attributes.create(type, GeneralizedTimeSyntax.format(highestFailureTime)); modifications.add(new Modification(ModificationType.ADD, addAttr, true)); // Now check to see if there have been sufficient failures to lock the account. int lockoutCount = passwordPolicy.getLockoutFailureCount(); if (lockoutCount > 0 && lockoutCount <= authFailureTimes.size()) { setFailureLockedTime(highestFailureTime); if (logger.isTraceEnabled()) { logger.trace("Locking user account %s due to too many failures.", userDNString); } } } /** * Explicitly specifies the auth failure times for the associated user. This should generally only be used * for testing purposes. Note that it will also set or clear the locked time as appropriate. * * @param authFailureTimes The set of auth failure times to use for the account. An empty list or * {@code null} will clear the account of any existing failures. */ public void setAuthFailureTimes(List<Long> authFailureTimes) { if (authFailureTimes == null || authFailureTimes.isEmpty()) { clearAuthFailureTimes(); clearFailureLockedTime(); return; } this.authFailureTimes = authFailureTimes; AttributeBuilder builder = new AttributeBuilder(OP_ATTR_PWPOLICY_FAILURE_TIME_LC); long highestFailureTime = -1; for (long l : authFailureTimes) { highestFailureTime = Math.max(l, highestFailureTime); builder.add(GeneralizedTimeSyntax.format(l)); } Attribute a = builder.toAttribute(); modifications.add(new Modification(ModificationType.REPLACE, a, true)); // Now check to see if there have been sufficient failures to lock the account. int lockoutCount = passwordPolicy.getLockoutFailureCount(); if (lockoutCount > 0 && lockoutCount <= authFailureTimes.size()) { setFailureLockedTime(highestFailureTime); if (logger.isTraceEnabled()) { logger.trace("Locking user account %s due to too many failures.", userDNString); } } } /** * Updates the user entry to remove any record of previous authentication failure times. */ private void clearAuthFailureTimes() { if (logger.isTraceEnabled()) { logger.trace("Clearing authentication failure times for user %s", userDNString); } List<Long> failureTimes = getAuthFailureTimes(); if (failureTimes.isEmpty()) { return; } failureTimes.clear(); // Note: failureTimes != this.authFailureTimes AttributeType type = DirectoryServer.getAttributeTypeOrDefault( OP_ATTR_PWPOLICY_FAILURE_TIME_LC, OP_ATTR_PWPOLICY_FAILURE_TIME); modifications.add(new Modification(ModificationType.REPLACE, Attributes.empty(type), true)); } /** * Retrieves the time of an authentication failure lockout for the user. * * @return The time of an authentication failure lockout for the user, or -1 if no such time is present in the entry. */ private long getFailureLockedTime() { if (failureLockedTime != Long.MIN_VALUE) { return failureLockedTime; } AttributeType type = DirectoryServer.getAttributeTypeOrDefault( OP_ATTR_PWPOLICY_LOCKED_TIME_LC, OP_ATTR_PWPOLICY_LOCKED_TIME); try { failureLockedTime = getGeneralizedTime(userEntry, type); } catch (Exception e) { logger.traceException(e, "Returning current time for user %s because an error occurred", userDNString); failureLockedTime = currentTime; return failureLockedTime; } // An expired locked time is handled in lockedDueToFailures. return failureLockedTime; } /** Sets the failure lockout attribute in the entry to the requested time. @param time The time to which to set the entry's failure lockout attribute. */ private void setFailureLockedTime(final long time) { if (time == getFailureLockedTime()) { return; } failureLockedTime = time; AttributeType type = DirectoryServer.getAttributeTypeOrDefault( OP_ATTR_PWPOLICY_LOCKED_TIME_LC, OP_ATTR_PWPOLICY_LOCKED_TIME); Attribute a = Attributes.create(type, GeneralizedTimeSyntax.format(failureLockedTime)); modifications.add(new Modification(ModificationType.REPLACE, a, true)); } /** * Updates the user entry to remove any record of previous authentication failure lockout. */ private void clearFailureLockedTime() { if (logger.isTraceEnabled()) { logger.trace("Clearing failure lockout time for user %s.", userDNString); } if (-1L == getFailureLockedTime()) { return; } failureLockedTime = -1L; AttributeType type = DirectoryServer.getAttributeTypeOrDefault( OP_ATTR_PWPOLICY_LOCKED_TIME_LC, OP_ATTR_PWPOLICY_LOCKED_TIME); modifications.add(new Modification(ModificationType.REPLACE, Attributes.empty(type), true)); } /** * Indicates whether the associated user should be considered locked out as a result of too many * authentication failures. In the case of an expired lock-out, this routine produces the update * to clear the lock-out attribute and the authentication failure timestamps. * In case the failure lockout time is absent from the entry, but sufficient authentication failure * timestamps are present in the entry, this routine produces the update to set the lock-out attribute. * * @return <CODE>true</CODE> if the user is currently locked out due to too many authentication failures, * or <CODE>false</CODE> if not. */ public boolean lockedDueToFailures() { // FIXME: Introduce a state field to cache the computed value of this method. // Note that only a cached "locked" status can be returned due to the possibility of intervening updates to // this.failureLockedTime by updateAuthFailureTimes. // Check if the feature is enabled in the policy. final int maxFailures = passwordPolicy.getLockoutFailureCount(); if (maxFailures <= 0) { if (logger.isTraceEnabled()) { logger.trace("Returning false for user %s because lockout due to failures is not enabled.", userDNString); } return false; } // Get the locked time from the user's entry. If it is present and not expired, the account is locked. // If it is absent, the failure timestamps must be checked, since failure timestamps sufficient to lock the // account could be produced across the synchronization topology within the synchronization latency. // Also, note that IETF draft-behera-ldap-password-policy-09 specifies "19700101000000Z" as the value to be set // under a "locked until reset" regime; however, this implementation accepts the value as a locked entry, // but observes the lockout expiration policy for all values including this one. // FIXME: This "getter" is unusual in that it might produce an update to the entry in two cases. // Does it make sense to factor the methods so that, e.g., an expired lockout is reported, and clearing // the lockout is left to the caller? if (getFailureLockedTime() < 0L) { // There was no locked time present in the entry; however, sufficient failure times might have accumulated // to trigger a lockout. if (getAuthFailureTimes().size() < maxFailures) { if (logger.isTraceEnabled()) { logger.trace("Returning false for user %s because there is no locked time.", userDNString); } return false; } // The account isn't locked but should be, so do so now. setFailureLockedTime(currentTime);// FIXME: set to max(failureTimes)? if (logger.isTraceEnabled()) { logger.trace("Locking user %s because there were enough existing failures even though there was" + " no account locked time.", userDNString); } // Fall through... } // There is a failure locked time, but it may be expired. if (passwordPolicy.getLockoutDuration() > 0) { final long unlockTime = getFailureLockedTime() + 1000L * passwordPolicy.getLockoutDuration(); if (unlockTime > currentTime) { secondsUntilUnlock = (int) ((unlockTime - currentTime) / 1000); if (logger.isTraceEnabled()) { logger.trace("Returning true for user %s because there is a locked time and the lockout duration has" + " not been reached.", userDNString); } return true; } // The lockout in the entry has expired... clearFailureLockout(); if (logger.isTraceEnabled()) { logger.trace("Returning false for user %s because the existing lockout has expired.", userDNString); } assert -1L == getFailureLockedTime(); return false; } if (logger.isTraceEnabled()) { logger.trace("Returning true for user %s because there is a locked time and no lockout duration.", userDNString); } assert -1L <= getFailureLockedTime(); return true; } /** * Retrieves the length of time in seconds until the user's account is automatically unlocked. * This should only be called after calling <CODE>lockedDueToFailures</CODE>. * * @return The length of time in seconds until the user's account is automatically unlocked, or -1 if the account * is not locked or the lockout requires administrative action to clear. */ public int getSecondsUntilUnlock() { // secondsUntilUnlock is only set when failureLockedTime is present and PasswordPolicy.getLockoutDuration // is enabled; hence it is not unreasonable to find secondsUntilUnlock uninitialized. assert failureLockedTime != Long.MIN_VALUE; return secondsUntilUnlock < 0 ? -1 : secondsUntilUnlock; } /** * Updates the user account to remove any record of a previous lockout due to failed authentications. */ public void clearFailureLockout() { clearAuthFailureTimes(); clearFailureLockedTime(); } /** * Retrieves the time that the user last authenticated to the Directory Server. * * @return The time that the user last authenticated to the Directory Server, or -1 if it cannot be determined. */ public long getLastLoginTime() { if (lastLoginTime != Long.MIN_VALUE) { if (logger.isTraceEnabled()) { logger.trace("Returning stored last login time of %d for user %s.", lastLoginTime, userDNString); } return lastLoginTime; } // The policy configuration must be checked since the entry cannot be evaluated without both an attribute // name and timestamp format. AttributeType type = passwordPolicy.getLastLoginTimeAttribute(); String format = passwordPolicy.getLastLoginTimeFormat(); if (type == null || format == null) { lastLoginTime = -1; if (logger.isTraceEnabled()) { logger.trace("Returning -1 for user %s because no last login time will be maintained.", userDNString); } return lastLoginTime; } boolean isGeneralizedTime = SYNTAX_GENERALIZED_TIME_NAME.equals(type.getSyntax().getName()); lastLoginTime = -1; List<Attribute> attrList = userEntry.getAttribute(type); if (attrList != null) { for (Attribute a : attrList) { if (a.isEmpty()) { continue; } String valueString = a.iterator().next().toString(); try { lastLoginTime = parseTime(format, valueString, isGeneralizedTime); if (logger.isTraceEnabled()) { logger.trace("Returning last login time of %d for user %s, decoded using current last login time format.", lastLoginTime, userDNString); } return lastLoginTime; } catch (Exception e) { logger.traceException(e); // This could mean that the last login time was encoded using a previous format. for (String f : passwordPolicy.getPreviousLastLoginTimeFormats()) { try { lastLoginTime = parseTime(f, valueString, isGeneralizedTime); if (logger.isTraceEnabled()) { logger.trace("Returning last login time of %d for user %s decoded using previous last login time " + "format of %s.", lastLoginTime, userDNString, f); } return lastLoginTime; } catch (Exception e2) { logger.traceException(e); } } assert lastLoginTime == -1; if (logger.isTraceEnabled()) { logger.trace("Returning -1 for user %s because the last login time value %s could not be parsed " + "using any known format.", userDNString, valueString); } return lastLoginTime; } } } assert lastLoginTime == -1; if (logger.isTraceEnabled()) { logger.trace("Returning %d for user %s because no last login time value exists.", lastLoginTime, userDNString); } return lastLoginTime; } private long parseTime(String format, String time, boolean isGeneralizedTime) throws ParseException { SimpleDateFormat dateFormat = new SimpleDateFormat(format); if (isGeneralizedTime) { dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); } return dateFormat.parse(time).getTime(); } /** * Updates the user entry to set the current time as the last login time. */ public void setLastLoginTime() { setLastLoginTime(currentTime); } /** * Updates the user entry to use the specified last login time. This should be used primarily for testing purposes, * as the variant that uses the current time should be used most of the time. * * @param lastLoginTime The last login time to set in the user entry. */ public void setLastLoginTime(long lastLoginTime) { AttributeType type = passwordPolicy.getLastLoginTimeAttribute(); String format = passwordPolicy.getLastLoginTimeFormat(); if (type == null || format == null) { return; } String timestamp; try { SimpleDateFormat dateFormat = new SimpleDateFormat(format); // If the attribute has a Generalized Time syntax, make it UTC time. if (SYNTAX_GENERALIZED_TIME_NAME.equals(type.getSyntax().getName())) { dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); } timestamp = dateFormat.format(new Date(lastLoginTime)); this.lastLoginTime = dateFormat.parse(timestamp).getTime(); } catch (Exception e) { logger.traceException(e, "Unable to set last login time for user %s because an error occurred", userDNString); return; } String existingTimestamp = getValue(type); if (existingTimestamp != null && timestamp.equals(existingTimestamp)) { logger.trace("Not updating last login time for user %s because the new value matches the existing value.", userDNString); return; } Attribute a = Attributes.create(type, timestamp); modifications.add(new Modification(ModificationType.REPLACE, a, true)); logger.trace("Updated the last login time for user %s to %s", userDNString, timestamp); } /** * Clears the last login time from the user's entry. This should generally be used only for testing purposes. */ public void clearLastLoginTime() { if (logger.isTraceEnabled()) { logger.trace("Clearing last login time for user %s", userDNString); } lastLoginTime = -1; modifications.add(new Modification(ModificationType.REPLACE, Attributes.empty(OP_ATTR_LAST_LOGIN_TIME), true)); } /** * Indicates whether the user's account is currently locked because it has been idle for too long. * * @return <CODE>true</CODE> if the user's account is locked because it has been idle for too long, * or <CODE>false</CODE> if not. */ public boolean lockedDueToIdleInterval() { if (isIdleLocked != ConditionResult.UNDEFINED) { if (logger.isTraceEnabled()) { logger.trace("Returning stored result of %b for user %s", isIdleLocked == ConditionResult.TRUE, userDNString); } return isIdleLocked == ConditionResult.TRUE; } // Return immediately if this feature is disabled, since the feature is not responsible for any state attribute // in the entry. if (passwordPolicy.getIdleLockoutInterval() <= 0) { isIdleLocked = ConditionResult.FALSE; if (logger.isTraceEnabled()) { logger.trace("Returning false for user %s because no idle lockout interval is defined.", userDNString); } return false; } long lockTime = currentTime - 1000L * passwordPolicy.getIdleLockoutInterval(); if (lockTime < 0) { lockTime = 0; } long theLastLoginTime = getLastLoginTime(); if (theLastLoginTime > lockTime || getPasswordChangedTime() > lockTime) { isIdleLocked = ConditionResult.FALSE; if (logger.isTraceEnabled()) { StringBuilder reason = new StringBuilder(); if(theLastLoginTime > lockTime) { reason.append("the last login time is in an acceptable window"); } else { if(theLastLoginTime < 0) { reason.append("there is no last login time, but "); } reason.append("the password changed time is in an acceptable window"); } logger.trace("Returning false for user %s because %s.", userDNString, reason); } } else { isIdleLocked = ConditionResult.TRUE; if (logger.isTraceEnabled()) { String reason = theLastLoginTime < 0 ? "there is no last login time and the password changed time is not in an acceptable window" : "neither last login time nor password changed time are in an acceptable window"; logger.trace("Returning true for user %s because %s.", userDNString, reason); } } return isIdleLocked == ConditionResult.TRUE; } /** * Indicates whether the user's password must be changed before any other operation can be performed. * * @return <CODE>true</CODE> if the user's password must be changed before any other operation can be performed. */ public boolean mustChangePassword() { if(mustChangePassword != ConditionResult.UNDEFINED) { if (logger.isTraceEnabled()) { logger.trace("Returning stored result of %b for user %s.", mustChangePassword == ConditionResult.TRUE, userDNString); } return mustChangePassword == ConditionResult.TRUE; } // If the password policy doesn't use force change on add or force change on reset, or if it forbids the user // from changing his password, then return false. // FIXME: the only getter responsible for a state attribute (pwdReset) that considers the policy before // checking the entry for the presence of the attribute. if (!passwordPolicy.isAllowUserPasswordChanges() || (!passwordPolicy.isForceChangeOnAdd() && !passwordPolicy.isForceChangeOnReset())) { mustChangePassword = ConditionResult.FALSE; if (logger.isTraceEnabled()) { logger.trace("Returning false for user %s because neither force change on add nor force change on reset" + " is enabled, or users are not allowed to self-modify passwords.", userDNString); } return false; } AttributeType type = DirectoryServer.getAttributeTypeOrDefault( OP_ATTR_PWPOLICY_RESET_REQUIRED_LC, OP_ATTR_PWPOLICY_RESET_REQUIRED); try { mustChangePassword = getBoolean(userEntry, type); } catch (Exception e) { logger.traceException(e, "Returning true for user %s because an error occurred", userDNString); mustChangePassword = ConditionResult.TRUE; return true; } if(mustChangePassword == ConditionResult.UNDEFINED) { mustChangePassword = ConditionResult.FALSE; logger.trace("Returning %b for user since the attribute \"%s\" is not present in the entry.", false, userDNString, OP_ATTR_PWPOLICY_RESET_REQUIRED); return false; } final boolean result = mustChangePassword == ConditionResult.TRUE; logger.trace("Returning %b for user %s.", result, userDNString); return result; } /** * Updates the user entry to indicate whether the user's password must be changed. * * @param mustChangePassword Indicates whether the user's password must be changed. */ public void setMustChangePassword(boolean mustChangePassword) { if (logger.isTraceEnabled()) { logger.trace("Updating user %s to set the reset flag to %b", userDNString, mustChangePassword); } if (mustChangePassword == mustChangePassword()) { return; // requested state matches current state } AttributeType type = DirectoryServer.getAttributeTypeOrDefault( OP_ATTR_PWPOLICY_RESET_REQUIRED_LC, OP_ATTR_PWPOLICY_RESET_REQUIRED); this.mustChangePassword = ConditionResult.not(this.mustChangePassword); if (mustChangePassword) { Attribute a = Attributes.create(type, String.valueOf(true)); modifications.add(new Modification(ModificationType.REPLACE, a, true)); } else { modifications.add(new Modification(ModificationType.REPLACE, Attributes.empty(type), true)); } } /** * Indicates whether the user's account is locked because the password has been reset by an administrator * but the user did not change the password in a timely manner. * * @return <CODE>true</CODE> if the user's account is locked because of the maximum reset age, * or <CODE>false</CODE> if not. */ public boolean lockedDueToMaximumResetAge() { // This feature is responsible for neither a state field nor an entry state attribute. if (passwordPolicy.getMaxPasswordResetAge() <= 0L) { if (logger.isTraceEnabled()) { logger.trace("Returning false for user %s because there is no maximum reset age.", userDNString); } return false; } if (! mustChangePassword()) { if (logger.isTraceEnabled()) { logger.trace("Returning false for user %s because the user's password has not been reset.", userDNString); } return false; } long maxResetTime = getPasswordChangedTime() + 1000L * passwordPolicy.getMaxPasswordResetAge(); boolean locked = maxResetTime < currentTime; if (logger.isTraceEnabled()) { logger.trace("Returning %b for user %s after comparing the current and max reset times.", locked, userDNString); } return locked; } /** * Returns whether the account was locked for any reason. * * @return true if the account is locked, false otherwise */ public boolean isLocked() { return lockedDueToIdleInterval() || lockedDueToMaximumResetAge() || lockedDueToFailures(); } /** * Retrieves the time that the user's password should expire (if the expiration is in the future) or * did expire (if the expiration was in the past). Note that this method should be called after the * <CODE>lockedDueToMaximumResetAge</CODE> method because grace logins will not be allowed in the case * that the maximum reset age has passed whereas they may be used for expiration due to maximum password * age or forced change time. * * @return The time that the user's password should/did expire, or -1 if it should not expire. */ public long getPasswordExpirationTime() { if (passwordExpirationTime == Long.MIN_VALUE) { passwordExpirationTime = Long.MAX_VALUE; boolean checkWarning = false; long maxAge = passwordPolicy.getMaxPasswordAge(); if (maxAge > 0L) { long expTime = getPasswordChangedTime() + 1000L * maxAge; if (expTime < passwordExpirationTime) { passwordExpirationTime = expTime; checkWarning = true; } } long maxResetAge = passwordPolicy.getMaxPasswordResetAge(); if (mustChangePassword() && maxResetAge > 0L) { long expTime = getPasswordChangedTime() + 1000L * maxResetAge; if (expTime < passwordExpirationTime) { passwordExpirationTime = expTime; checkWarning = false; } } long mustChangeTime = passwordPolicy.getRequireChangeByTime(); if (mustChangeTime > 0) { long reqChangeTime = getRequiredChangeTime(); if (reqChangeTime != mustChangeTime && mustChangeTime < passwordExpirationTime) { passwordExpirationTime = mustChangeTime; checkWarning = true; } } if (passwordExpirationTime == Long.MAX_VALUE) { passwordExpirationTime = -1; shouldWarn = ConditionResult.FALSE; isFirstWarning = ConditionResult.FALSE; isPasswordExpired = ConditionResult.FALSE; mayUseGraceLogin = ConditionResult.TRUE; } else if (checkWarning) { mayUseGraceLogin = ConditionResult.TRUE; long warningInterval = passwordPolicy.getPasswordExpirationWarningInterval(); if (warningInterval > 0L) { long shouldWarnTime = passwordExpirationTime - warningInterval * 1000L; if (shouldWarnTime > currentTime) { // The warning time is in the future, so we know the password isn't expired. shouldWarn = ConditionResult.FALSE; isFirstWarning = ConditionResult.FALSE; isPasswordExpired = ConditionResult.FALSE; } else { // We're at least in the warning period, but the password may be expired. long theWarnedTime = getWarnedTime(); if (passwordExpirationTime > currentTime) { // The password is not expired but we should warn the user. shouldWarn = ConditionResult.TRUE; isPasswordExpired = ConditionResult.FALSE; if (theWarnedTime < 0) { isFirstWarning = ConditionResult.TRUE; setWarnedTime(); if (! passwordPolicy.isExpirePasswordsWithoutWarning()) { passwordExpirationTime = currentTime + warningInterval * 1000L; } } else { isFirstWarning = ConditionResult.FALSE; if (! passwordPolicy.isExpirePasswordsWithoutWarning()) { passwordExpirationTime = theWarnedTime + warningInterval * 1000L; } } } else { // The expiration time has passed, but we may not actually be expired if the user has not // yet seen a warning. if (passwordPolicy.isExpirePasswordsWithoutWarning()) { shouldWarn = ConditionResult.FALSE; isFirstWarning = ConditionResult.FALSE; isPasswordExpired = ConditionResult.TRUE; } else if (theWarnedTime > 0) { passwordExpirationTime = theWarnedTime + warningInterval*1000L; if (passwordExpirationTime > currentTime) { shouldWarn = ConditionResult.TRUE; isFirstWarning = ConditionResult.FALSE; isPasswordExpired = ConditionResult.FALSE; } else { shouldWarn = ConditionResult.FALSE; isFirstWarning = ConditionResult.FALSE; isPasswordExpired = ConditionResult.TRUE; } } else { shouldWarn = ConditionResult.TRUE; isFirstWarning = ConditionResult.TRUE; isPasswordExpired = ConditionResult.FALSE; passwordExpirationTime = currentTime + warningInterval*1000L; } } } } else { // There will never be a warning, and the user's password may be expired. shouldWarn = ConditionResult.FALSE; isFirstWarning = ConditionResult.FALSE; if (currentTime > passwordExpirationTime) { isPasswordExpired = ConditionResult.TRUE; } else { isPasswordExpired = ConditionResult.FALSE; } } } else { mayUseGraceLogin = ConditionResult.FALSE; shouldWarn = ConditionResult.FALSE; isFirstWarning = ConditionResult.FALSE; if (passwordExpirationTime < currentTime) { isPasswordExpired = ConditionResult.TRUE; } else { isPasswordExpired = ConditionResult.FALSE; } } } if (logger.isTraceEnabled()) { logger.trace("Returning password expiration time of %d for user %s.", passwordExpirationTime, userDNString); } return passwordExpirationTime; } /** * Indicates whether the user's password is currently expired. * * @return <CODE>true</CODE> if the user's password is currently expired, or <CODE>false</CODE> if not. */ public boolean isPasswordExpired() { refreshIfUndefined(isPasswordExpired); return isPasswordExpired == ConditionResult.TRUE; } private void refreshIfUndefined(ConditionResult cond) { if (cond == null || cond == ConditionResult.UNDEFINED) { getPasswordExpirationTime(); } } /** * Indicates whether the user's last password change was within the minimum password age. * * @return <CODE>true</CODE> if the password minimum age is nonzero, the account is not in force-change mode, * and the last password change was within the minimum age, or <CODE>false</CODE> otherwise. */ public boolean isWithinMinimumAge() { // This feature is responsible for neither a state field nor entry state attribute. long minAge = passwordPolicy.getMinPasswordAge(); if (minAge <= 0L) { // There is no minimum age, so the user isn't in it. if (logger.isTraceEnabled()) { logger.trace("Returning false because there is no minimum age."); } return false; } else if (getPasswordChangedTime() + minAge * 1000L < currentTime) { // It's been long enough since the user changed their password. if (logger.isTraceEnabled()) { logger.trace("Returning false because the minimum age has expired."); } return false; } else if (mustChangePassword()) { // The user is in a must-change mode, so the minimum age doesn't apply. if (logger.isTraceEnabled()) { logger.trace("Returning false because the account is in a must-change state."); } return false; } else { // The user is within the minimum age. if (logger.isTraceEnabled()) { logger.trace("Returning true."); } return true; } } /** * Indicates whether the user may use a grace login if the password is expired and there is at least one * grace login remaining. Note that this does not check to see if the user's password is expired, does not * verify that there are any remaining grace logins, and does not update the set of grace login times. * * @return <CODE>true</CODE> if the user may use a grace login if the password is expired and there is * at least one grace login remaining, or <CODE>false</CODE> if the user may not use a grace * login for some reason. */ public boolean mayUseGraceLogin() { refreshIfUndefined(mayUseGraceLogin); return mayUseGraceLogin == ConditionResult.TRUE; } /** * Indicates whether the user should receive a warning notification that the password is about to expire. * * @return <CODE>true</CODE> if the user should receive a warning notification that the password is about to expire, * or <CODE>false</CODE> if not. */ public boolean shouldWarn() { refreshIfUndefined(shouldWarn); return shouldWarn == ConditionResult.TRUE; } /** * Indicates whether the warning that the user should receive would be the first warning for the user. * * @return <CODE>true</CODE> if the warning that should be sent to the user would be the first warning, * or <CODE>false</CODE> if not. */ public boolean isFirstWarning() { refreshIfUndefined(isFirstWarning); return isFirstWarning == ConditionResult.TRUE; } /** * Retrieves the length of time in seconds until the user's password expires. * * @return The length of time in seconds until the user's password expires, * 0 if the password is currently expired, or -1 if the password should not expire. */ public int getSecondsUntilExpiration() { long expirationTime = getPasswordExpirationTime(); if (expirationTime < 0) { return -1; } else if (expirationTime < currentTime) { return 0; } else { return (int) ((expirationTime - currentTime) / 1000); } } /** * Retrieves the timestamp for the last required change time that the user complied with. * * @return The timestamp for the last required change time that the user complied with, * or -1 if the user's password has not been changed in compliance with this configuration. */ public long getRequiredChangeTime() { if (requiredChangeTime != Long.MIN_VALUE) { if (logger.isTraceEnabled()) { logger.trace("Returning stored required change time of %d for user %s", requiredChangeTime, userDNString); } return requiredChangeTime; } try { requiredChangeTime = getGeneralizedTime0(userEntry, OP_ATTR_PWPOLICY_CHANGED_BY_REQUIRED_TIME); } catch (Exception e) { logger.traceException(e, "Returning %d for user %s because an error occurred", requiredChangeTime, userDNString); requiredChangeTime = -1; return requiredChangeTime; } logger.trace("Returning required change time of %d for user %s", requiredChangeTime, userDNString); return requiredChangeTime; } /** * Updates the user entry with a timestamp indicating that the password has been changed in accordance * with the require change time. */ public void setRequiredChangeTime() { long requiredChangeByTimePolicy = passwordPolicy.getRequireChangeByTime(); if (requiredChangeByTimePolicy > 0) { setRequiredChangeTime(requiredChangeByTimePolicy); } } /** * Updates the user entry with a timestamp indicating that the password has been changed in accordance * with the require change time. * * @param requiredChangeTime The timestamp to use for the required change time value. */ public void setRequiredChangeTime(long requiredChangeTime) { if (logger.isTraceEnabled()) { logger.trace("Updating required change time for user %s", userDNString); } if (getRequiredChangeTime() != requiredChangeTime) { this.requiredChangeTime = requiredChangeTime; String timeValue = GeneralizedTimeSyntax.format(requiredChangeTime); Attribute a = Attributes.create(OP_ATTR_PWPOLICY_CHANGED_BY_REQUIRED_TIME, timeValue); modifications.add(new Modification(ModificationType.REPLACE, a, true)); } } /** * Updates the user entry to remove any timestamp indicating that the password has been changed in accordance * with the required change time. */ public void clearRequiredChangeTime() { if (logger.isTraceEnabled()) { logger.trace("Clearing required change time for user %s", userDNString); } this.requiredChangeTime = Long.MIN_VALUE; String attrName = OP_ATTR_PWPOLICY_CHANGED_BY_REQUIRED_TIME; modifications.add(new Modification(ModificationType.REPLACE, Attributes.empty(attrName), true)); } /** * Retrieves the time that the user was first warned about an upcoming expiration. * * @return The time that the user was first warned about an upcoming expiration, or -1 if the user has * not been warned. */ public long getWarnedTime() { if (warnedTime == Long.MIN_VALUE) { try { warnedTime = getGeneralizedTime0(userEntry, OP_ATTR_PWPOLICY_WARNED_TIME); } catch (Exception e) { logger.traceException(e, "Unable to decode the warned time for user %s", userDNString); warnedTime = -1; } } logger.trace("Returning a warned time of %d for user %s", warnedTime, userDNString); return warnedTime; } /** * Updates the user entry to set the warned time to the current time. */ public void setWarnedTime() { setWarnedTime(currentTime); } /** * Updates the user entry to set the warned time to the specified time. This method should generally * only be used for testing purposes, since the variant that uses the current time is preferred almost * everywhere else. * * @param warnedTime The value to use for the warned time. */ public void setWarnedTime(long warnedTime) { long warnTime = getWarnedTime(); if (warnTime == warnedTime) { if (logger.isTraceEnabled()) { logger.trace("Not updating warned time for user %s because the warned time is the same as the specified time.", userDNString); } return; } this.warnedTime = warnedTime; AttributeType type = DirectoryServer.getAttributeTypeOrDefault(OP_ATTR_PWPOLICY_WARNED_TIME); Attribute a = Attributes.create(type, GeneralizedTimeSyntax.createGeneralizedTimeValue(currentTime)); modifications.add(new Modification(ModificationType.REPLACE, a, true)); if (logger.isTraceEnabled()) { logger.trace("Updated the warned time for user %s", userDNString); } } /** * Updates the user entry to clear the warned time. */ public void clearWarnedTime() { if (logger.isTraceEnabled()) { logger.trace("Clearing warned time for user %s", userDNString); } if (getWarnedTime() < 0) { return; } warnedTime = -1; String attrName = OP_ATTR_PWPOLICY_WARNED_TIME; modifications.add(new Modification(ModificationType.REPLACE, Attributes.empty(attrName), true)); if (logger.isTraceEnabled()) { logger.trace("Cleared the warned time for user %s", userDNString); } } /** * Retrieves the times that the user has authenticated to the server using a grace login. * * @return The times that the user has authenticated to the server using a grace login. */ public List<Long> getGraceLoginTimes() { if (graceLoginTimes == null) { AttributeType type = DirectoryServer.getAttributeTypeOrDefault( OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME_LC, OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME); try { graceLoginTimes = getGeneralizedTimes(type); } catch (Exception e) { logger.traceException(e, "Error while processing grace login times for user %s", userDNString); graceLoginTimes = new ArrayList<>(); modifications.add(new Modification(ModificationType.REPLACE, Attributes.empty(type), true)); } } logger.trace("Returning grace login times for user %s", userDNString); return graceLoginTimes; } /** * Retrieves the number of grace logins that the user has left. * * @return The number of grace logins that the user has left, or -1 if grace logins are not allowed. */ public int getGraceLoginsRemaining() { int maxGraceLogins = passwordPolicy.getGraceLoginCount(); if (maxGraceLogins <= 0) { return -1; } List<Long> theGraceLoginTimes = getGraceLoginTimes(); return maxGraceLogins - theGraceLoginTimes.size(); } /** * Updates the set of grace login times for the user to include the current time. */ public void updateGraceLoginTimes() { if (logger.isTraceEnabled()) { logger.trace("Updating grace login times for user %s", userDNString); } List<Long> graceTimes = getGraceLoginTimes(); long highestGraceTime = computeHighestTime(graceTimes); graceTimes.add(highestGraceTime); // graceTimes == this.graceLoginTimes AttributeType type = DirectoryServer.getAttributeTypeOrDefault( OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME_LC, OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME); Attribute addAttr = Attributes.create(type, GeneralizedTimeSyntax.format(highestGraceTime)); modifications.add(new Modification(ModificationType.ADD, addAttr, true)); } private long computeHighestTime(List<Long> graceTimes) { long highestTime = -1; for (long l : graceTimes) { highestTime = Math.max(l, highestTime); } if (highestTime >= currentTime) { highestTime++; } else { highestTime = currentTime; } return highestTime; } /** * Specifies the set of grace login use times for the associated user. If the provided list is empty * or {@code null}, then the set will be cleared. * * @param graceLoginTimes The grace login use times for the associated user. */ public void setGraceLoginTimes(List<Long> graceLoginTimes) { if (graceLoginTimes == null || graceLoginTimes.isEmpty()) { clearGraceLoginTimes(); return; } if (logger.isTraceEnabled()) { logger.trace("Updating grace login times for user %s", userDNString); } this.graceLoginTimes = graceLoginTimes; AttributeBuilder builder = new AttributeBuilder(OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME_LC); for (long l : graceLoginTimes) { builder.add(GeneralizedTimeSyntax.format(l)); } Attribute a = builder.toAttribute(); modifications.add(new Modification(ModificationType.REPLACE, a, true)); } /** * Updates the user entry to remove any record of previous grace logins. */ public void clearGraceLoginTimes() { if (logger.isTraceEnabled()) { logger.trace("Clearing grace login times for user %s", userDNString); } List<Long> graceTimes = getGraceLoginTimes(); if (graceTimes.isEmpty()) { return; } graceTimes.clear(); // graceTimes == this.graceLoginTimes AttributeType type = DirectoryServer.getAttributeTypeOrDefault( OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME_LC, OP_ATTR_PWPOLICY_GRACE_LOGIN_TIME); modifications.add(new Modification(ModificationType.REPLACE, Attributes.empty(type), true)); } /** * Retrieves a list of the clear-text passwords for the user. If the user does not have any passwords * in the clear, then the list will be empty. * * @return A list of the clear-text passwords for the user. */ public List<ByteString> getClearPasswords() { LinkedList<ByteString> clearPasswords = new LinkedList<>(); List<Attribute> attrList = userEntry.getAttribute(passwordPolicy.getPasswordAttribute()); if (attrList == null) { return clearPasswords; } for (Attribute a : attrList) { for (ByteString v : a) { try { String[] pwComponents = getPwComponents(v); String schemeName = pwComponents[0]; PasswordStorageScheme<?> scheme = getPasswordStorageScheme(schemeName); if (scheme == null) { if (logger.isTraceEnabled()) { logger.trace("User entry %s contains a password with scheme %s that is not defined in the server.", userDNString, schemeName); } continue; } if (scheme.isReversible()) { clearPasswords.add(getPlaintextValue(scheme, pwComponents)); } } catch (Exception e) { logger.traceException(e); if (logger.isTraceEnabled()) { logger.trace("Cannot get clear password value for user %s: %s", userDNString, e); } } } } return clearPasswords; } private ByteString getPlaintextValue(PasswordStorageScheme<?> scheme, String[] pwComponents) throws DirectoryException { return passwordPolicy.isAuthPasswordSyntax() ? scheme.getAuthPasswordPlaintextValue(pwComponents[1], pwComponents[2]) : scheme.getPlaintextValue(ByteString.valueOfUtf8(pwComponents[1])); } @Override public boolean passwordMatches(ByteString password) { List<Attribute> attrList = userEntry.getAttribute(passwordPolicy.getPasswordAttribute()); if (attrList == null || attrList.isEmpty()) { if (logger.isTraceEnabled()) { logger.trace("Returning false because user %s does not have any values for password attribute %s", userDNString, passwordPolicy.getPasswordAttribute().getNameOrOID()); } return false; } for (Attribute a : attrList) { for (ByteString v : a) { try { String[] pwComponents = getPwComponents(v); String schemeName = pwComponents[0]; PasswordStorageScheme<?> scheme = getPasswordStorageScheme(schemeName); if (scheme == null) { if (logger.isTraceEnabled()) { logger.trace("User entry %s contains a password with scheme %s that is not defined in the server.", userDNString, schemeName); } continue; } if (passwordMatches(password, pwComponents, scheme)) { if (logger.isTraceEnabled()) { logger.trace("Returning true for user %s because the provided password matches a value " + "encoded with scheme %s", userDNString, schemeName); } return true; } } catch (Exception e) { logger.traceException(e, "An error occurred while attempting to process a password value for user %s", userDNString); } } } // If we've gotten here, then we couldn't find a match. logger.trace("Returning false because the provided password does not match any of the stored password " + "values for user %s", userDNString); return false; } /** * Get the broken-down components of the given password value. * * @param usesAuthPasswordSyntax true if the value is an authPassword. * @param v The encoded password value to break down. * * @return An array of components. */ private String[] getPwComponents(ByteString v) throws DirectoryException { return passwordPolicy.isAuthPasswordSyntax() ? AuthPasswordSyntax.decodeAuthPassword(v.toString()) : UserPasswordSyntax.decodeUserPassword(v.toString()); } /** * Indicates whether the provided password value is pre-encoded. * * @param passwordValue The value for which to make the determination. * * @return <CODE>true</CODE> if the provided password value is pre-encoded, or <CODE>false</CODE> if it is not. */ public boolean passwordIsPreEncoded(ByteString passwordValue) { return passwordPolicy.isAuthPasswordSyntax() ? AuthPasswordSyntax.isEncoded(passwordValue) : UserPasswordSyntax.isEncoded(passwordValue); } /** * Encodes the provided password using the default storage schemes (using the appropriate syntax for the * password attribute). * * @param password The password to be encoded. * * @return The password encoded using the default schemes. * * @throws DirectoryException If a problem occurs while attempting to encode the password. */ public List<ByteString> encodePassword(ByteString password) throws DirectoryException { List<PasswordStorageScheme<?>> schemes = passwordPolicy.getDefaultPasswordStorageSchemes(); List<ByteString> encodedPasswords = new ArrayList<>(schemes.size()); if (passwordPolicy.isAuthPasswordSyntax()) { for (PasswordStorageScheme<?> s : schemes) { encodedPasswords.add(s.encodeAuthPassword(password)); } } else { for (PasswordStorageScheme<?> s : schemes) { encodedPasswords.add(s.encodePasswordWithScheme(password)); } } return encodedPasswords; } /** * Indicates whether the provided password appears to be acceptable according to the password validators. * * @param operation The operation that provided the password. * @param userEntry The user entry in which the password is used. * @param newPassword The password to be validated. * @param currentPasswords The set of clear-text current passwords for the user (this may be a subset * if not all of them are available in the clear, or empty if none of them * are available in the clear). * @param invalidReason A buffer that may be used to hold the invalid reason if the password is rejected. * * @return <CODE>true</CODE> if the password is acceptable for use, or <CODE>false</CODE> if it is not. */ public boolean passwordIsAcceptable(Operation operation, Entry userEntry, ByteString newPassword, Set<ByteString> currentPasswords, LocalizableMessageBuilder invalidReason) { for (PasswordValidator<?> validator : passwordPolicy.getPasswordValidators()) { if (!validator.passwordIsAcceptable(newPassword, currentPasswords, operation, userEntry, invalidReason)) { if (logger.isTraceEnabled()) { logger.trace("The password provided for user %s failed validation: %s", userDNString, invalidReason); } return false; } } return true; } /** * Performs any processing that may be necessary to remove deprecated storage schemes from the user's entry * that match the provided password and re-encodes them using the default schemes. * * @param password The clear-text password provided by the user. */ public void handleDeprecatedStorageSchemes(ByteString password) { if (passwordPolicy.getDeprecatedPasswordStorageSchemes().isEmpty()) { if (logger.isTraceEnabled()) { logger.trace("Doing nothing for user %s because no deprecated storage schemes have been defined.", userDNString); } return; } AttributeType type = passwordPolicy.getPasswordAttribute(); List<Attribute> attrList = userEntry.getAttribute(type); if (attrList == null || attrList.isEmpty()) { if (logger.isTraceEnabled()) { logger.trace("Doing nothing for entry %s because no password values were found.", userDNString); } return; } HashSet<String> existingDefaultSchemes = new HashSet<>(); LinkedHashSet<ByteString> removedValues = new LinkedHashSet<>(); LinkedHashSet<ByteString> updatedValues = new LinkedHashSet<>(); for (Attribute a : attrList) { for (ByteString v : a) { try { String[] pwComponents = getPwComponents(v); String schemeName = pwComponents[0]; PasswordStorageScheme<?> scheme = getPasswordStorageScheme(schemeName); if (scheme == null) { if (logger.isTraceEnabled()) { logger.trace("Skipping password value for user %s because the associated storage scheme %s " + "is not configured for use.", userDNString, schemeName); } continue; } if (passwordMatches(password, pwComponents, scheme)) { if (passwordPolicy.isDefaultPasswordStorageScheme(schemeName)) { existingDefaultSchemes.add(schemeName); updatedValues.add(v); } else if (passwordPolicy.isDeprecatedPasswordStorageScheme(schemeName)) { if (logger.isTraceEnabled()) { logger.trace("Marking password with scheme %s for removal from user entry %s.", schemeName, userDNString); } removedValues.add(v); } else { updatedValues.add(v); } } } catch (Exception e) { logger.traceException(e, "Skipping password value for user %s because an error occurred while attempting " + "to decode it based on the user password syntax", userDNString); } } } if (removedValues.isEmpty()) { logger.trace("User entry %s does not have any password values encoded using deprecated schemes.", userDNString); return; } LinkedHashSet<ByteString> addedValues = new LinkedHashSet<>(); for (PasswordStorageScheme<?> s : passwordPolicy.getDefaultPasswordStorageSchemes()) { if (! existingDefaultSchemes.contains(toLowerCase(s.getStorageSchemeName()))) { try { ByteString encodedPassword = encodePassword(password, s); addedValues.add(encodedPassword); updatedValues.add(encodedPassword); } catch (Exception e) { logger.traceException(e); if (logger.isTraceEnabled()) { logger.traceException(e, "Unable to encode password for user %s using default scheme %s", userDNString, s.getStorageSchemeName()); } } } } if (updatedValues.isEmpty()) { logger.trace( "Not updating user entry %s because removing deprecated schemes would leave the user without a password.", userDNString); return; } Attribute a = newAttribute(type, removedValues); modifications.add(new Modification(ModificationType.DELETE, a, true)); if (! addedValues.isEmpty()) { Attribute a2 = newAttribute(type, addedValues); modifications.add(new Modification(ModificationType.ADD, a2, true)); } if (logger.isTraceEnabled()) { logger.trace("Updating user entry %s to replace password values encoded with deprecated schemes " + "with values encoded with the default schemes.", userDNString); } } private PasswordStorageScheme<?> getPasswordStorageScheme(String schemeName) { return passwordPolicy.isAuthPasswordSyntax() ? DirectoryServer.getAuthPasswordStorageScheme(schemeName) : DirectoryServer.getPasswordStorageScheme(schemeName); } private boolean passwordMatches(ByteString password, String[] pwComponents, PasswordStorageScheme<?> scheme) { return passwordPolicy.isAuthPasswordSyntax() ? scheme.authPasswordMatches(password, pwComponents[1], pwComponents[2]) : scheme.passwordMatches(password, ByteString.valueOfUtf8(pwComponents[1])); } private ByteString encodePassword(ByteString password, PasswordStorageScheme<?> s) throws DirectoryException { return passwordPolicy.isAuthPasswordSyntax() ? s.encodeAuthPassword(password) : s.encodePasswordWithScheme(password); } /** * Indicates whether password history information should be maintained for this user. * * @return {@code true} if password history information should be maintained for this user, or {@code false} if not. */ public boolean maintainHistory() { return passwordPolicy.getPasswordHistoryCount() > 0 || passwordPolicy.getPasswordHistoryDuration() > 0; } /** * Indicates whether the provided password is equal to any of the current passwords, * or any of the passwords in the history. * * @param password The password for which to make the determination. * * @return {@code true} if the provided password is equal to any of the current passwords or any of the passwords * in the history, or {@code false} if not. */ public boolean isPasswordInHistory(ByteString password) { if (! maintainHistory()) { if (logger.isTraceEnabled()) { logger.trace("Returning false because password history checking is disabled."); } return false; } // Check to see if the provided password is equal to any of the current passwords. // If so, then we'll consider it to be in the history. if (passwordMatches(password)) { if (logger.isTraceEnabled()) { logger.trace("Returning true because the provided password is currently in use."); } return true; } // Get the attribute containing the history and check to see if any of the values is equal to the provided password. // However, first prune the list by size and duration if necessary. TreeMap<Long, ByteString> historyMap = getSortedHistoryValues(null); int historyCount = passwordPolicy.getPasswordHistoryCount(); if (historyCount > 0 && historyMap.size() > historyCount) { int numToDelete = historyMap.size() - historyCount; Iterator<Long> iterator = historyMap.keySet().iterator(); while (iterator.hasNext() && numToDelete > 0) { iterator.next(); iterator.remove(); numToDelete--; } } long historyDuration = passwordPolicy.getPasswordHistoryDuration(); if (historyDuration > 0L) { long retainDate = currentTime - 1000 * historyDuration; Iterator<Long> iterator = historyMap.keySet().iterator(); while (iterator.hasNext()) { long historyDate = iterator.next(); if (historyDate >= retainDate) { break; } iterator.remove(); } } for (ByteString v : historyMap.values()) { if (historyValueMatches(password, v)) { if (logger.isTraceEnabled()) { logger.trace("Returning true because the password is in the history."); } return true; } } // If we've gotten here, then the password isn't in the history. if (logger.isTraceEnabled()) { logger.trace("Returning false because the password isn't in the history."); } return false; } /** * Gets a sorted list of the password history values contained in the user's entry. * The values will be sorted by timestamp. * * @param removeAttrs A list into which any values will be placed that could not be properly decoded. * It may be {@code null} if this is not needed. */ private TreeMap<Long,ByteString> getSortedHistoryValues(List<Attribute> removeAttrs) { TreeMap<Long, ByteString> historyMap = new TreeMap<>(); AttributeType historyType = DirectoryServer.getAttributeTypeOrDefault(OP_ATTR_PWPOLICY_HISTORY_LC); List<Attribute> attrList = userEntry.getAttribute(historyType); if (attrList != null) { for (Attribute a : attrList) { for (ByteString v : a) { String histStr = v.toString(); int hashPos = histStr.indexOf('#'); if (hashPos <= 0) { if (logger.isTraceEnabled()) { logger.trace("Found value " + histStr + " in the history with no timestamp. Marking it for removal."); } if (removeAttrs != null) { removeAttrs.add(Attributes.create(a.getAttributeType(), v)); } } else { try { ByteString timeValue = ByteString.valueOfUtf8(histStr.substring(0, hashPos)); long timestamp = GeneralizedTimeSyntax.decodeGeneralizedTimeValue(timeValue); historyMap.put(timestamp, v); } catch (Exception e) { if (logger.isTraceEnabled()) { logger.traceException(e); logger.trace("Could not decode the timestamp in history value " + histStr + " -- " + e + ". Marking it for removal."); } if (removeAttrs != null) { removeAttrs.add(Attributes.create(a.getAttributeType(), v)); } } } } } } return historyMap; } /** * Indicates whether the provided password matches the given history value. * * @param password The clear-text password for which to make the determination. * @param historyValue The encoded history value to compare against the clear-text password. * * @return {@code true} if the provided password matches the history value, or {@code false} if not. */ private boolean historyValueMatches(ByteString password, ByteString historyValue) { // According to draft-behera-ldap-password-policy, password history values should be in the format // time#syntaxoid#encodedvalue. In this method, we only care about the syntax OID and encoded password. try { String histStr = historyValue.toString(); int hashPos1 = histStr.indexOf('#'); if (hashPos1 <= 0) { if (logger.isTraceEnabled()) { logger.trace("Returning false because the password history value didn't include any hash characters."); } return false; } int hashPos2 = histStr.indexOf('#', hashPos1+1); if (hashPos2 < 0) { if (logger.isTraceEnabled()) { logger.trace("Returning false because the password history value only had one hash character."); } return false; } String syntaxOID = toLowerCase(histStr.substring(hashPos1+1, hashPos2)); if (SYNTAX_AUTH_PASSWORD_OID.equals(syntaxOID)) { return logResult("auth", encodedAuthPasswordMatches(password, histStr.substring(hashPos2+1))); } else if (SYNTAX_USER_PASSWORD_OID.equals(syntaxOID)) { return logResult("user", encodedUserPasswordMatches(password, histStr.substring(hashPos2+1))); } else { if (logger.isTraceEnabled()) { logger.trace("Returning false because the syntax OID " + syntaxOID + " didn't match for either the auth or user password syntax."); } return false; } } catch (Exception e) { if (logger.isTraceEnabled()) { logger.traceException(e); logger.trace("Returning false because of an exception: " + stackTraceToSingleLineString(e)); } return false; } } private boolean encodedAuthPasswordMatches(ByteString password, String encodedAuthPassword) throws DirectoryException { String[] authPWComponents = AuthPasswordSyntax.decodeAuthPassword(encodedAuthPassword); PasswordStorageScheme<?> scheme = DirectoryServer.getAuthPasswordStorageScheme(authPWComponents[0]); return scheme.authPasswordMatches(password, authPWComponents[1], authPWComponents[2]); } private boolean encodedUserPasswordMatches(ByteString password, String encodedUserPassword) throws DirectoryException { String[] userPWComponents = UserPasswordSyntax.decodeUserPassword(encodedUserPassword); PasswordStorageScheme<?> scheme = DirectoryServer.getPasswordStorageScheme(userPWComponents[0]); return scheme.passwordMatches(password, ByteString.valueOfUtf8(userPWComponents[1])); } private boolean logResult(String passwordType, boolean passwordMatches) { if (passwordMatches) { logger.trace("Returning true because the %s password history value matched.", passwordType); return true; } else { logger.trace("Returning false because the %s password history value did not match.", passwordType); return false; } } /** * Updates the password history information for this user by adding one of the passwords to it. * It will choose the first password encoded using a secure storage scheme, and will fall back to * a password encoded using an insecure storage scheme if necessary. */ public void updatePasswordHistory() { List<Attribute> attrList = userEntry.getAttribute(passwordPolicy.getPasswordAttribute()); if (attrList != null) { for (Attribute a : attrList) { ByteString insecurePassword = null; for (ByteString v : a) { try { PasswordStorageScheme<?> scheme = getPasswordStorageScheme(v); if (scheme.isStorageSchemeSecure()) { addPasswordToHistory(v.toString()); insecurePassword = null; // no need to check any more values for this attribute break; } else if (insecurePassword == null) { insecurePassword = v; } } catch (DirectoryException e) { if (logger.isTraceEnabled()) { logger.trace("Encoded password " + v + " cannot be decoded and cannot be added to history."); } } } // If we get here we haven't found a password encoded securely, so we have to use one of the other values. if (insecurePassword != null) { addPasswordToHistory(insecurePassword.toString()); } } } } /** * Adds the provided password to the password history. If appropriate, one or more old passwords may be * evicted from the list if the total size would exceed the configured count, or if passwords are older * than the configured duration. * * @param encodedPassword The encoded password (in either user password or auth password format) * to be added to the history. */ private void addPasswordToHistory(String encodedPassword) { if (! maintainHistory()) { if (logger.isTraceEnabled()) { logger.trace("Not doing anything because password history maintenance is disabled."); } return; } // Get a sorted list of the existing values to see if there are any that should be removed. LinkedList<Attribute> removeAttrs = new LinkedList<>(); TreeMap<Long, ByteString> historyMap = getSortedHistoryValues(removeAttrs); // If there is a maximum number of values to retain and we would be over the limit with the new value, // then get rid of enough values (oldest first) to satisfy the count. AttributeType historyType = DirectoryServer.getAttributeTypeOrDefault(OP_ATTR_PWPOLICY_HISTORY_LC); int historyCount = passwordPolicy.getPasswordHistoryCount(); if (historyCount > 0 && historyMap.size() >= historyCount) { int numToDelete = historyMap.size() - historyCount + 1; LinkedHashSet<ByteString> removeValues = new LinkedHashSet<>(numToDelete); Iterator<ByteString> iterator = historyMap.values().iterator(); while (iterator.hasNext() && numToDelete > 0) { ByteString v = iterator.next(); removeValues.add(v); iterator.remove(); numToDelete--; if (logger.isTraceEnabled()) { logger.trace("Removing history value %s to preserve the history count.", v); } } if (! removeValues.isEmpty()) { removeAttrs.add(newAttribute(historyType, removeValues)); } } // If there is a maximum duration, then get rid of any values that would be over the duration. long historyDuration = passwordPolicy.getPasswordHistoryDuration(); if (historyDuration > 0L) { long minAgeToKeep = currentTime - 1000L * historyDuration; Iterator<Long> iterator = historyMap.keySet().iterator(); LinkedHashSet<ByteString> removeValues = new LinkedHashSet<>(); while (iterator.hasNext()) { long timestamp = iterator.next(); if (timestamp >= minAgeToKeep) { break; } ByteString v = historyMap.get(timestamp); removeValues.add(v); iterator.remove(); if (logger.isTraceEnabled()) { logger.trace("Removing history value %s to preserve the history duration.", v); } } if (! removeValues.isEmpty()) { removeAttrs.add(newAttribute(historyType, removeValues)); } } // At this point, we can add the new value. However, we want to make sure that its timestamp // (which is the current time) doesn't conflict with any value already in the list. If there is a conflict, // then simply add one to it until we don't have any more conflicts. long newTimestamp = currentTime; while (historyMap.containsKey(newTimestamp)) { newTimestamp++; } String newHistStr = GeneralizedTimeSyntax.format(newTimestamp) + "#" + passwordPolicy.getPasswordAttribute().getSyntax().getOID() + "#" + encodedPassword; Attribute newHistAttr = Attributes.create(historyType, newHistStr); if (logger.isTraceEnabled()) { logger.trace("Going to add history value " + newHistStr); } // Apply the changes, either by adding modifications or by directly updating the entry. for (Attribute a : removeAttrs) { modifications.add(new Modification(ModificationType.DELETE, a, true)); } modifications.add(new Modification(ModificationType.ADD, newHistAttr, true)); } private Attribute newAttribute(AttributeType type, LinkedHashSet<ByteString> values) { AttributeBuilder builder = new AttributeBuilder(type); builder.addAll(values); return builder.toAttribute(); } /** * Retrieves the password history state values for the user. This is only intended for testing purposes. * * @return The password history state values for the user. */ public String[] getPasswordHistoryValues() { ArrayList<String> historyValues = new ArrayList<>(); AttributeType historyType = DirectoryServer.getAttributeTypeOrDefault(OP_ATTR_PWPOLICY_HISTORY_LC); List<Attribute> attrList = userEntry.getAttribute(historyType); if (attrList != null) { for (Attribute a : attrList) { for (ByteString v : a) { historyValues.add(v.toString()); } } } return historyValues.toArray(new String[historyValues.size()]); } /** * Clears the password history state information for the user. This is only intended for testing purposes. */ public void clearPasswordHistory() { if (logger.isTraceEnabled()) { logger.trace("Clearing password history for user %s", userDNString); } modifications.add(new Modification(ModificationType.REPLACE, Attributes.empty(OP_ATTR_PWPOLICY_HISTORY_LC), true)); } /** * Generates a new password for the user. * * @return The new password that has been generated, or <CODE>null</CODE> if no password generator has been defined. * * @throws DirectoryException If an error occurs while attempting to generate the new password. */ public ByteString generatePassword() throws DirectoryException { PasswordGenerator<?> generator = passwordPolicy.getPasswordGenerator(); if (generator == null) { if (logger.isTraceEnabled()) { logger.trace("Unable to generate a new password for user %s because no password generator has been defined" + "in the associated password policy.", userDNString); } return null; } return generator.generatePassword(userEntry); } /** * Generates an account status notification for this user. * * @param notificationType The type for the account status notification. * @param userEntry The entry for the user to which this notification applies. * @param message The human-readable message for the notification. * @param notificationProperties The set of properties for the notification. */ public void generateAccountStatusNotification( AccountStatusNotificationType notificationType, Entry userEntry, LocalizableMessage message, Map<AccountStatusNotificationProperty,List<String>> notificationProperties) { generateAccountStatusNotification( new AccountStatusNotification(notificationType, userEntry, message, notificationProperties)); } /** * Generates an account status notification for this user. * * @param notification The account status notification that should be generated. */ public void generateAccountStatusNotification(AccountStatusNotification notification) { Collection<AccountStatusNotificationHandler<?>> handlers = passwordPolicy.getAccountStatusNotificationHandlers(); for (AccountStatusNotificationHandler<?> handler : handlers) { handler.handleStatusNotification(notification); } } /** * Retrieves the set of modifications that correspond to changes made in password policy processing * that may need to be applied to the user entry. * * @return The set of modifications that correspond to changes made in password policy processing * that may need to be applied to the user entry. */ public List<Modification> getModifications() { return modifications; } @Override public void finalizeStateAfterBind() throws DirectoryException { // If there are no modifications, then there's nothing to do. if (modifications.isEmpty()) { return; } // Convert the set of modifications to a set of LDAP modifications. ArrayList<RawModification> modList = new ArrayList<>(); for (Modification m : modifications) { modList.add(RawModification.create(m.getModificationType(), new LDAPAttribute(m.getAttribute()))); } InternalClientConnection conn = getRootConnection(); ModifyOperation internalModify = conn.processModify(ByteString.valueOfUtf8(userDNString), modList); ResultCode resultCode = internalModify.getResultCode(); if (resultCode != ResultCode.SUCCESS) { LocalizableMessage message = ERR_PWPSTATE_CANNOT_UPDATE_USER_ENTRY.get( userDNString, internalModify.getErrorMessage()); // If this is a root user, or if the password policy says that we should ignore these problems, // then log a warning message. Otherwise, cause the bind to fail. if (DirectoryServer.isRootDN(userEntry.getName()) || passwordPolicy.getStateUpdateFailurePolicy() == PasswordPolicyCfgDefn.StateUpdateFailurePolicy.IGNORE) { logger.error(message); } else { throw new DirectoryException(resultCode, message); } } } }