Implement support for password history functionality. The password history can
be maintained either based on the number of previous passwords to remember
(e.g., a user cannot re-use any of his/her last five passwords), or the length
of time the previous passwords have been retained (e.g., a user cannot re-use
any password he/she has had within the last 365 days), or both.
This commit also updates the manage-account tool so that it supports a public
option for displaying the password history state values, and a hidden option
for clearing a user's password history state.
OpenDS Issue Number: 339
| | |
| | | ds-cfg-require-secure-password-changes: false |
| | | ds-cfg-skip-validation-for-administrators: false |
| | | ds-cfg-state-update-failure-policy: reactive |
| | | ds-cfg-password-history-count: 0 |
| | | ds-cfg-password-history-duration: 0 seconds |
| | | |
| | | dn: cn=Root Password Policy,cn=Password Policies,cn=config |
| | | objectClass: top |
| | |
| | | ds-cfg-require-secure-password-changes: false |
| | | ds-cfg-skip-validation-for-administrators: false |
| | | ds-cfg-state-update-failure-policy: ignore |
| | | ds-cfg-password-history-count: 0 |
| | | ds-cfg-password-history-duration: 0 seconds |
| | | |
| | | dn: cn=Password Storage Schemes,cn=config |
| | | objectClass: top |
| | |
| | | attributeTypes: ( 1.3.6.1.4.1.26027.1.1.443 |
| | | NAME 'ds-cfg-state-update-failure-policy' |
| | | SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'OpenDS Directory Server' ) |
| | | attributeTypes: ( 1.3.6.1.4.1.26027.1.1.444 |
| | | NAME 'ds-cfg-password-history-count' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 |
| | | X-ORIGIN 'OpenDS Directory Server' ) |
| | | attributeTypes: ( 1.3.6.1.4.1.26027.1.1.445 |
| | | NAME 'ds-cfg-password-history-duration' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 |
| | | X-ORIGIN 'OpenDS Directory Server' ) |
| | | objectClasses: ( 1.3.6.1.4.1.26027.1.2.1 |
| | | NAME 'ds-cfg-access-control-handler' SUP top STRUCTURAL |
| | | MUST ( cn $ ds-cfg-acl-handler-class $ ds-cfg-acl-handler-enabled ) |
| | |
| | | ds-cfg-require-change-by-time $ ds-cfg-require-secure-authentication $ |
| | | ds-cfg-require-secure-password-changes $ |
| | | ds-cfg-skip-validation-for-administrators $ |
| | | ds-cfg-state-update-failure-policy ) X-ORIGIN 'OpenDS Directory Server' ) |
| | | ds-cfg-state-update-failure-policy $ ds-cfg-password-history-count $ |
| | | ds-cfg-password-history-duration ) X-ORIGIN 'OpenDS Directory Server' ) |
| | | objectClasses: ( 1.3.6.1.4.1.26027.1.2.63 NAME |
| | | 'ds-cfg-jmx-connection-handler' SUP ds-cfg-connection-handler |
| | | STRUCTURAL MUST ( ds-cfg-listen-port $ ds-cfg-ssl-cert-nickname $ |
| | |
| | | <ldap:attribute> |
| | | <ldap:oid>1.3.6.1.4.1.26027.1.1.443</ldap:oid> |
| | | <ldap:name>ds-cfg-state-update-failure-policy</ldap:name> |
| | | </ldap:attribute> |
| | | </adm:profile> |
| | | </adm:property> |
| | | |
| | | <adm:property name="password-history-count" mandatory="false" |
| | | multi-valued="false"> |
| | | <adm:synopsis> |
| | | Specifies the maximum number of former passwords to maintain in the |
| | | password history. When choosing a new password, the proposed password |
| | | will be checked to ensure that it does not match the current password, nor |
| | | any other password in the history list. A value of zero indicates that |
| | | either no password history is to be maintained (if the password history |
| | | duration has a value of zero seconds), or that there is no maximum number |
| | | of passwords to maintain in the history (if the password history duration |
| | | has a value greater than zero seconds). |
| | | </adm:synopsis> |
| | | <adm:default-behavior> |
| | | <adm:defined> |
| | | <adm:value>0</adm:value> |
| | | </adm:defined> |
| | | </adm:default-behavior> |
| | | <adm:syntax> |
| | | <adm:integer lower-limit="0" upper-limit="2147483647" /> |
| | | </adm:syntax> |
| | | <adm:profile name="ldap"> |
| | | <ldap:attribute> |
| | | <ldap:oid>1.3.6.1.4.1.26027.1.1.444</ldap:oid> |
| | | <ldap:name>ds-cfg-password-history-count</ldap:name> |
| | | </ldap:attribute> |
| | | </adm:profile> |
| | | </adm:property> |
| | | |
| | | <adm:property name="password-history-duration" mandatory="false" |
| | | multi-valued="false"> |
| | | <adm:synopsis> |
| | | Specifies the maximum length of time that passwords should remain in the |
| | | password history. When choosing a new password, the proposed password |
| | | will be checked to ensure that it does not match the current password, nor |
| | | any other password in the history list. A value of zero seconds indicates |
| | | that either no password history is to be maintained (if the password |
| | | history count has a value of zero), or that there is no maximum duration |
| | | for passwords in the history (if the password history count has a value |
| | | greater than zero). |
| | | </adm:synopsis> |
| | | <adm:default-behavior> |
| | | <adm:defined> |
| | | <adm:value>0 seconds</adm:value> |
| | | </adm:defined> |
| | | </adm:default-behavior> |
| | | <adm:syntax> |
| | | <adm:duration base-unit="s" lower-limit="0" upper-limit="2147483647" |
| | | allow-unlimited="false" /> |
| | | </adm:syntax> |
| | | <adm:profile name="ldap"> |
| | | <ldap:attribute> |
| | | <ldap:oid>1.3.6.1.4.1.26027.1.1.445</ldap:oid> |
| | | <ldap:name>ds-cfg-password-history-duration</ldap:name> |
| | | </ldap:attribute> |
| | | </adm:profile> |
| | | </adm:property> |
| | |
| | | |
| | | |
| | | /** |
| | | * The default value for the password history count configuration attribute. |
| | | */ |
| | | public static final int DEFAULT_PWPOLICY_HISTORY_COUNT = 0; |
| | | |
| | | |
| | | |
| | | /** |
| | | * The default value for the password history duration configuration |
| | | * attribute, in seconds. |
| | | */ |
| | | public static final int DEFAULT_PWPOLICY_HISTORY_DURATION = 0; |
| | | |
| | | |
| | | |
| | | /** |
| | | * The name of the configuration attribute that specifies the maximum length |
| | | * of time an account may remain idle. |
| | | */ |
| | |
| | | |
| | | |
| | | /** |
| | | * The name of the operational attribute that is used to maintain the password |
| | | * history for the user. |
| | | */ |
| | | public static final String OP_ATTR_PWPOLICY_HISTORY = "pwdHistory"; |
| | | |
| | | |
| | | |
| | | /** |
| | | * The name of the operational attribute that is used to maintain the password |
| | | * history for the user, in all lowercase characters. |
| | | */ |
| | | public static final String OP_ATTR_PWPOLICY_HISTORY_LC = "pwdhistory"; |
| | | |
| | | |
| | | |
| | | /** |
| | | * The name of the operational attribute that specifies the time that the |
| | | * account was locked due to too many failed attempts. |
| | | */ |
| | |
| | | // The number of grace logins that a user may have. |
| | | private int graceLoginCount = DEFAULT_PWPOLICY_GRACE_LOGIN_COUNT; |
| | | |
| | | // The number of passwords to keep in the history. |
| | | private int historyCount = DEFAULT_PWPOLICY_HISTORY_COUNT; |
| | | |
| | | // The maximum length of time in seconds to keep passwords in the history. |
| | | private int historyDuration = DEFAULT_PWPOLICY_HISTORY_DURATION; |
| | | |
| | | // The maximum length of time in seconds that an account may remain idle |
| | | // before it is locked out. |
| | | private int idleLockoutInterval = DEFAULT_PWPOLICY_IDLE_LOCKOUT_INTERVAL; |
| | |
| | | this.stateUpdateFailurePolicy = configuration.getStateUpdateFailurePolicy(); |
| | | |
| | | |
| | | // Get the password history count and duration. |
| | | this.historyCount = configuration.getPasswordHistoryCount(); |
| | | this.historyDuration = (int) configuration.getPasswordHistoryDuration(); |
| | | |
| | | |
| | | /* |
| | | * Holistic validation. |
| | | */ |
| | |
| | | |
| | | |
| | | /** |
| | | * Retrieves the maximum number of previous passwords to maintain in the |
| | | * password history. |
| | | * |
| | | * @return The maximum number of previous passwords to maintain in the |
| | | * password history. |
| | | */ |
| | | public int getPasswordHistoryCount() |
| | | { |
| | | return historyCount; |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Retrieves the maximum length of time in seconds that previous passwords |
| | | * should remain in the password history. |
| | | * |
| | | * @return The maximum length of time in seconds that previous passwords |
| | | * should remain in the password history. |
| | | */ |
| | | public int getPasswordHistoryDuration() |
| | | { |
| | | return historyDuration; |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Indicates whether users with this password policy will be required to |
| | | * authenticate in a secure manner that does not expose their password. |
| | | * |
| | |
| | | buffer.append(idleLockoutInterval); |
| | | buffer.append(" seconds"); |
| | | buffer.append(EOL); |
| | | |
| | | buffer.append("History Count: "); |
| | | buffer.append(historyCount); |
| | | buffer.append(EOL); |
| | | |
| | | buffer.append("Update Failure Policy: "); |
| | | buffer.append(stateUpdateFailurePolicy.toString()); |
| | | buffer.append(EOL); |
| | | } |
| | | } |
| | | |
| | |
| | | import java.util.LinkedList; |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | import java.util.TreeMap; |
| | | |
| | | import org.opends.server.admin.std.meta.PasswordPolicyCfgDefn; |
| | | import org.opends.server.admin.std.server.PasswordValidatorCfg; |
| | |
| | | import static org.opends.server.loggers.debug.DebugLogger.*; |
| | | import static org.opends.server.messages.CoreMessages.*; |
| | | import static org.opends.server.messages.MessageHandler.*; |
| | | import static org.opends.server.schema.SchemaConstants.*; |
| | | import static org.opends.server.util.StaticUtils.*; |
| | | |
| | | |
| | |
| | | |
| | | |
| | | /** |
| | | * Indicates whether password history information should be matained 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 (debug) |
| | | { |
| | | if (debugEnabled()) |
| | | { |
| | | TRACER.debugInfo("Returning false because password history " + |
| | | "checking is disabled."); |
| | | } |
| | | } |
| | | |
| | | // Password history checking is disabled, so we don't care if it is in the |
| | | // list or not. |
| | | 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 (debug) |
| | | { |
| | | if (debugEnabled()) |
| | | { |
| | | TRACER.debugInfo("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,AttributeValue> 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--; |
| | | } |
| | | } |
| | | |
| | | int historyDuration = passwordPolicy.getPasswordHistoryDuration(); |
| | | if (historyDuration > 0) |
| | | { |
| | | long retainDate = currentTime - (1000 * historyDuration); |
| | | Iterator<Long> iterator = historyMap.keySet().iterator(); |
| | | while (iterator.hasNext()) |
| | | { |
| | | long historyDate = iterator.next(); |
| | | if (historyDate < retainDate) |
| | | { |
| | | iterator.remove(); |
| | | } |
| | | else |
| | | { |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | |
| | | for (AttributeValue v : historyMap.values()) |
| | | { |
| | | if (historyValueMatches(password, v)) |
| | | { |
| | | if (debug) |
| | | { |
| | | if (debugEnabled()) |
| | | { |
| | | TRACER.debugInfo("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 (debug) |
| | | { |
| | | if (debugEnabled()) |
| | | { |
| | | TRACER.debugInfo("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,AttributeValue> getSortedHistoryValues(List<Attribute> |
| | | removeAttrs) |
| | | { |
| | | TreeMap<Long,AttributeValue> historyMap = |
| | | new TreeMap<Long,AttributeValue>(); |
| | | AttributeType historyType = |
| | | DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_HISTORY_LC, true); |
| | | List<Attribute> attrList = userEntry.getAttribute(historyType); |
| | | if (attrList != null) |
| | | { |
| | | for (Attribute a : attrList) |
| | | { |
| | | for (AttributeValue v : a.getValues()) |
| | | { |
| | | String histStr = v.getStringValue(); |
| | | int hashPos = histStr.indexOf('#'); |
| | | if (hashPos <= 0) |
| | | { |
| | | if (debug) |
| | | { |
| | | if (debugEnabled()) |
| | | { |
| | | TRACER.debugInfo("Found value " + histStr + " in the " + |
| | | "history with no timestamp. Marking it " + |
| | | "for removal."); |
| | | } |
| | | } |
| | | |
| | | LinkedHashSet<AttributeValue> values = |
| | | new LinkedHashSet<AttributeValue>(1); |
| | | values.add(v); |
| | | if (removeAttrs != null) |
| | | { |
| | | removeAttrs.add(new Attribute(a.getAttributeType(), a.getName(), |
| | | values)); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | try |
| | | { |
| | | long timestamp = |
| | | GeneralizedTimeSyntax.decodeGeneralizedTimeValue( |
| | | new ASN1OctetString(histStr.substring(0, hashPos))); |
| | | historyMap.put(timestamp, v); |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | if (debugEnabled()) |
| | | { |
| | | TRACER.debugCaught(DebugLogLevel.ERROR, e); |
| | | |
| | | if (debug) |
| | | { |
| | | TRACER.debugInfo("Could not decode the timestamp in " + |
| | | "history value " + histStr + " -- " + e + |
| | | ". Marking it for removal."); |
| | | } |
| | | } |
| | | |
| | | LinkedHashSet<AttributeValue> values = |
| | | new LinkedHashSet<AttributeValue>(1); |
| | | values.add(v); |
| | | if (removeAttrs != null) |
| | | { |
| | | removeAttrs.add(new Attribute(a.getAttributeType(), a.getName(), |
| | | values)); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | 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, |
| | | AttributeValue 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.getStringValue(); |
| | | int hashPos1 = histStr.indexOf('#'); |
| | | if (hashPos1 <= 0) |
| | | { |
| | | if (debug) |
| | | { |
| | | if (debugEnabled()) |
| | | { |
| | | TRACER.debugInfo("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 (debug) |
| | | { |
| | | if (debugEnabled()) |
| | | { |
| | | TRACER.debugInfo("Returning false because the password history " + |
| | | "value only had one hash character."); |
| | | } |
| | | } |
| | | |
| | | return false; |
| | | } |
| | | |
| | | String syntaxOID = toLowerCase(histStr.substring(hashPos1+1, hashPos2)); |
| | | if (syntaxOID.equals(SYNTAX_AUTH_PASSWORD_OID)) |
| | | { |
| | | StringBuilder[] authPWComponents = |
| | | AuthPasswordSyntax.decodeAuthPassword( |
| | | histStr.substring(hashPos2+1)); |
| | | PasswordStorageScheme scheme = |
| | | DirectoryServer.getAuthPasswordStorageScheme( |
| | | authPWComponents[0].toString()); |
| | | if (scheme.authPasswordMatches(password, authPWComponents[1].toString(), |
| | | authPWComponents[2].toString())) |
| | | { |
| | | if (debug) |
| | | { |
| | | if (debugEnabled()) |
| | | { |
| | | TRACER.debugInfo("Returning true because the auth password " + |
| | | "history value matched."); |
| | | } |
| | | } |
| | | |
| | | return true; |
| | | } |
| | | else |
| | | { |
| | | if (debug) |
| | | { |
| | | if (debugEnabled()) |
| | | { |
| | | TRACER.debugInfo("Returning false because the auth password " + |
| | | "history value did not match."); |
| | | } |
| | | } |
| | | |
| | | return false; |
| | | } |
| | | } |
| | | else if (syntaxOID.equals(SYNTAX_USER_PASSWORD_OID)) |
| | | { |
| | | String[] userPWComponents = |
| | | UserPasswordSyntax.decodeUserPassword( |
| | | histStr.substring(hashPos2+1)); |
| | | PasswordStorageScheme scheme = |
| | | DirectoryServer.getPasswordStorageScheme(userPWComponents[0]); |
| | | if (scheme.passwordMatches(password, |
| | | new ASN1OctetString(userPWComponents[1]))) |
| | | { |
| | | if (debug) |
| | | { |
| | | if (debugEnabled()) |
| | | { |
| | | TRACER.debugInfo("Returning true because the user password " + |
| | | "history value matched."); |
| | | } |
| | | } |
| | | |
| | | return true; |
| | | } |
| | | else |
| | | { |
| | | if (debug) |
| | | { |
| | | if (debugEnabled()) |
| | | { |
| | | TRACER.debugInfo("Returning false because the user password " + |
| | | "history value did not match."); |
| | | } |
| | | } |
| | | |
| | | return false; |
| | | } |
| | | } |
| | | else |
| | | { |
| | | if (debug) |
| | | { |
| | | if (debugEnabled()) |
| | | { |
| | | TRACER.debugInfo("Returning false because the syntax OID " + |
| | | syntaxOID + " didn't match for either the auth " + |
| | | "or user password syntax."); |
| | | } |
| | | } |
| | | |
| | | return false; |
| | | } |
| | | } |
| | | catch (Exception e) |
| | | { |
| | | if (debugEnabled()) |
| | | { |
| | | TRACER.debugCaught(DebugLogLevel.ERROR, e); |
| | | |
| | | if (debug) |
| | | { |
| | | TRACER.debugInfo("Returning false because of an exception: " + |
| | | stackTraceToSingleLineString(e)); |
| | | } |
| | | } |
| | | |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Updates the password history information for this user by adding all |
| | | * current passwords to it. |
| | | */ |
| | | public void updatePasswordHistory() |
| | | { |
| | | List<Attribute> attrList = |
| | | userEntry.getAttribute(passwordPolicy.getPasswordAttribute()); |
| | | if (attrList != null) |
| | | { |
| | | for (Attribute a : attrList) |
| | | { |
| | | for (AttributeValue v : a.getValues()) |
| | | { |
| | | addPasswordToHistory(v.getStringValue()); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * 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 (debug) |
| | | { |
| | | if (debugEnabled()) |
| | | { |
| | | TRACER.debugInfo("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<Attribute>(); |
| | | TreeMap<Long,AttributeValue> 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.getAttributeType(OP_ATTR_PWPOLICY_HISTORY_LC, true); |
| | | int historyCount = passwordPolicy.getPasswordHistoryCount(); |
| | | if ((historyCount > 0) && (historyMap.size() >= historyCount)) |
| | | { |
| | | int numToDelete = (historyMap.size() - historyCount) + 1; |
| | | LinkedHashSet<AttributeValue> removeValues = |
| | | new LinkedHashSet<AttributeValue>(numToDelete); |
| | | Iterator<AttributeValue> iterator = historyMap.values().iterator(); |
| | | while (iterator.hasNext() && (numToDelete > 0)) |
| | | { |
| | | AttributeValue v = iterator.next(); |
| | | removeValues.add(v); |
| | | iterator.remove(); |
| | | numToDelete--; |
| | | |
| | | if (debug) |
| | | { |
| | | if (debugEnabled()) |
| | | { |
| | | TRACER.debugInfo("Removing history value " + v.getStringValue() + |
| | | " to preserve the history count."); |
| | | } |
| | | } |
| | | } |
| | | |
| | | if (! removeValues.isEmpty()) |
| | | { |
| | | removeAttrs.add(new Attribute(historyType, historyType.getPrimaryName(), |
| | | removeValues)); |
| | | } |
| | | } |
| | | |
| | | |
| | | // If there is a maximum duration, then get rid of any values that would be |
| | | // over the duration. |
| | | int historyDuration = passwordPolicy.getPasswordHistoryDuration(); |
| | | if (historyDuration > 0) |
| | | { |
| | | long minAgeToKeep = currentTime - (1000L * historyDuration); |
| | | Iterator<Long> iterator = historyMap.keySet().iterator(); |
| | | LinkedHashSet<AttributeValue> removeValues = |
| | | new LinkedHashSet<AttributeValue>(); |
| | | while (iterator.hasNext()) |
| | | { |
| | | long timestamp = iterator.next(); |
| | | if (timestamp < minAgeToKeep) |
| | | { |
| | | AttributeValue v = historyMap.get(timestamp); |
| | | removeValues.add(v); |
| | | iterator.remove(); |
| | | |
| | | if (debug) |
| | | { |
| | | if (debugEnabled()) |
| | | { |
| | | TRACER.debugInfo("Removing history value " + v.getStringValue() + |
| | | " to preserve the history duration."); |
| | | } |
| | | } |
| | | } |
| | | else |
| | | { |
| | | break; |
| | | } |
| | | } |
| | | |
| | | if (! removeValues.isEmpty()) |
| | | { |
| | | removeAttrs.add(new Attribute(historyType, historyType.getPrimaryName(), |
| | | 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().getSyntaxOID() + |
| | | "#" + encodedPassword; |
| | | LinkedHashSet<AttributeValue> newHistValues = |
| | | new LinkedHashSet<AttributeValue>(1); |
| | | newHistValues.add(new AttributeValue(historyType, newHistStr)); |
| | | Attribute newHistAttr = |
| | | new Attribute(historyType, historyType.getPrimaryName(), |
| | | newHistValues); |
| | | |
| | | if (debug) |
| | | { |
| | | if (debugEnabled()) |
| | | { |
| | | TRACER.debugInfo("Going to add history value " + newHistStr); |
| | | } |
| | | } |
| | | |
| | | |
| | | // Apply the changes, either by adding modifications or by directly updating |
| | | // the entry. |
| | | if (updateEntry) |
| | | { |
| | | LinkedList<AttributeValue> valueList = new LinkedList<AttributeValue>(); |
| | | for (Attribute a : removeAttrs) |
| | | { |
| | | userEntry.removeAttribute(a, valueList); |
| | | } |
| | | |
| | | userEntry.addAttribute(newHistAttr, valueList); |
| | | } |
| | | else |
| | | { |
| | | for (Attribute a : removeAttrs) |
| | | { |
| | | modifications.add(new Modification(ModificationType.DELETE, a, true)); |
| | | } |
| | | |
| | | modifications.add(new Modification(ModificationType.ADD, newHistAttr, |
| | | true)); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * 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<String>(); |
| | | AttributeType historyType = |
| | | DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_HISTORY_LC, true); |
| | | List<Attribute> attrList = userEntry.getAttribute(historyType); |
| | | if (attrList != null) |
| | | { |
| | | for (Attribute a : attrList) |
| | | { |
| | | for (AttributeValue v : a.getValues()) |
| | | { |
| | | historyValues.add(v.getStringValue()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | String[] historyArray = new String[historyValues.size()]; |
| | | return historyValues.toArray(historyArray); |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Clears the password history state information for the user. This is only |
| | | * intended for testing purposes. |
| | | */ |
| | | public void clearPasswordHistory() |
| | | { |
| | | if (debug) |
| | | { |
| | | if (debugEnabled()) |
| | | { |
| | | TRACER.debugInfo("Clearing password history for user %s", userDNString); |
| | | } |
| | | } |
| | | |
| | | AttributeType type = DirectoryServer.getAttributeType( |
| | | OP_ATTR_PWPOLICY_HISTORY_LC, true); |
| | | if (updateEntry) |
| | | { |
| | | userEntry.removeAttribute(type); |
| | | } |
| | | else |
| | | { |
| | | modifications.add(new Modification(ModificationType.REPLACE, |
| | | new Attribute(type), true)); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Generates a new password for the user. |
| | | * |
| | | * @return The new password that has been generated, or <CODE>null</CODE> if |
| | |
| | | } |
| | | else |
| | | { |
| | | // Run the new password through the set of password validators. |
| | | if (selfChange || |
| | | (! pwPolicyState.getPolicy().skipValidationForAdministrators())) |
| | | { |
| | |
| | | return; |
| | | } |
| | | } |
| | | |
| | | |
| | | // Prepare to update the password history, if necessary. |
| | | if (pwPolicyState.maintainHistory()) |
| | | { |
| | | if (pwPolicyState.isPasswordInHistory(newPassword)) |
| | | { |
| | | if (oldPassword == null) |
| | | { |
| | | operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM); |
| | | |
| | | int msgID = MSGID_EXTOP_PASSMOD_PW_IN_HISTORY; |
| | | operation.appendErrorMessage(getMessage(msgID)); |
| | | } |
| | | else |
| | | { |
| | | operation.setResultCode(ResultCode.INVALID_CREDENTIALS); |
| | | |
| | | int msgID = MSGID_EXTOP_PASSMOD_PW_IN_HISTORY; |
| | | operation.appendAdditionalLogMessage(getMessage(msgID)); |
| | | } |
| | | } |
| | | else |
| | | { |
| | | pwPolicyState.updatePasswordHistory(); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | * setPasswordChangedByRequiredTime (36), |
| | | * clearPasswordChangedByRequiredTime (37), |
| | | * getSecondsUntilRequiredChangeTime (38), |
| | | * getPasswordHistory (39), |
| | | * clearPasswordHistory (40), |
| | | * ... }, |
| | | * opValues SEQUENCE OF OCTET STRING OPTIONAL } |
| | | * </PRE> |
| | |
| | | |
| | | |
| | | |
| | | /** |
| | | * The enumerated value for the getPasswordHistory operation. |
| | | */ |
| | | public static final int OP_GET_PASSWORD_HISTORY = 39; |
| | | |
| | | |
| | | |
| | | /** |
| | | * The enumerated value for the clearPasswordHistory operation. |
| | | */ |
| | | public static final int OP_CLEAR_PASSWORD_HISTORY = 40; |
| | | |
| | | |
| | | |
| | | // The set of attributes to request when retrieving a user's entry. |
| | | private LinkedHashSet<String> requestAttributes; |
| | | |
| | |
| | | returnTypes.add(OP_GET_SECONDS_UNTIL_REQUIRED_CHANGE_TIME); |
| | | break; |
| | | |
| | | case OP_GET_PASSWORD_HISTORY: |
| | | returnTypes.add(OP_GET_PASSWORD_HISTORY); |
| | | break; |
| | | |
| | | case OP_CLEAR_PASSWORD_HISTORY: |
| | | pwpState.clearPasswordHistory(); |
| | | returnTypes.add(OP_GET_PASSWORD_HISTORY); |
| | | break; |
| | | |
| | | default: |
| | | int msgID = MSGID_PWPSTATE_EXTOP_UNKNOWN_OP_TYPE; |
| | | operation.appendErrorMessage(getMessage(msgID, opType)); |
| | |
| | | secondsStr)); |
| | | } |
| | | |
| | | if (returnAll || returnTypes.contains(OP_GET_PASSWORD_HISTORY)) |
| | | { |
| | | opElements.add(encode(OP_GET_PASSWORD_HISTORY, |
| | | pwpState.getPasswordHistoryValues())); |
| | | } |
| | | |
| | | ArrayList<ASN1Element> responseValueElements = |
| | | new ArrayList<ASN1Element>(2); |
| | | responseValueElements.add(dnString); |
| | |
| | | |
| | | |
| | | /** |
| | | * The message ID for the message that will be used if a new password is found |
| | | * in the password history. This does not take any arguments. |
| | | */ |
| | | public static final int MSGID_MODIFY_PW_IN_HISTORY = |
| | | CATEGORY_MASK_CORE | SEVERITY_MASK_MILD_ERROR | 629; |
| | | |
| | | |
| | | |
| | | /** |
| | | * Associates a set of generic messages with the message IDs defined |
| | | * in this class. |
| | | */ |
| | |
| | | registerMessage(MSGID_MODIFY_PW_VALIDATION_FAILED, |
| | | "The provided password value was rejected by a password " + |
| | | "validator: %s"); |
| | | registerMessage(MSGID_MODIFY_PW_IN_HISTORY, |
| | | "The provided new password was found in the password " + |
| | | "history for the user"); |
| | | registerMessage(MSGID_MODIFY_INCREMENT_REQUIRES_INTEGER_VALUE, |
| | | "Entry %s cannot be modified because an attempt was " + |
| | | "made to increment the value of attribute %s but the " + |
| | |
| | | |
| | | |
| | | /** |
| | | * The message ID for the message that will be used if a password change is |
| | | * rejected because the new password was found in the password history. This |
| | | * does not take any arguments. |
| | | */ |
| | | public static final int MSGID_EXTOP_PASSMOD_PW_IN_HISTORY = |
| | | CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 530; |
| | | |
| | | |
| | | |
| | | /** |
| | | * Associates a set of generic messages with the message IDs defined in this |
| | | * class. |
| | | */ |
| | |
| | | registerMessage(MSGID_EXTOP_PASSMOD_UNACCEPTABLE_PW, |
| | | "The provided new password failed the validation checks " + |
| | | "defined in the server: %s"); |
| | | registerMessage(MSGID_EXTOP_PASSMOD_PW_IN_HISTORY, |
| | | "The provided new password was already contained in the " + |
| | | "password history"); |
| | | registerMessage(MSGID_EXTOP_PASSMOD_CANNOT_ENCODE_PASSWORD, |
| | | "Unable to encode the provided password using the " + |
| | | "default scheme(s): %s"); |
| | |
| | | |
| | | |
| | | /** |
| | | * The message ID for the message that will be used as the label when |
| | | * displaying the password history state values. |
| | | */ |
| | | public static final int MSGID_PWPSTATE_LABEL_PASSWORD_HISTORY = |
| | | CATEGORY_MASK_TOOLS | SEVERITY_MASK_INFORMATIONAL | 1200; |
| | | |
| | | |
| | | |
| | | /** |
| | | * The message ID for the message that will be used as the description for the |
| | | * get-password-history subcommand. |
| | | */ |
| | | public static final int MSGID_DESCRIPTION_PWPSTATE_GET_PASSWORD_HISTORY = |
| | | CATEGORY_MASK_TOOLS | SEVERITY_MASK_INFORMATIONAL | 1201; |
| | | |
| | | |
| | | |
| | | /** |
| | | * The message ID for the message that will be used as the description for the |
| | | * clear-password-history subcommand. |
| | | */ |
| | | public static final int MSGID_DESCRIPTION_PWPSTATE_CLEAR_PASSWORD_HISTORY = |
| | | CATEGORY_MASK_TOOLS | SEVERITY_MASK_INFORMATIONAL | 1202; |
| | | |
| | | |
| | | |
| | | /** |
| | | * Associates a set of generic messages with the message IDs defined in this |
| | | * class. |
| | | */ |
| | |
| | | "Display the length of time in seconds that the user has remaining " + |
| | | "to change his or her password before the account becomes locked " + |
| | | "due to the required change time"); |
| | | registerMessage(MSGID_DESCRIPTION_PWPSTATE_GET_PASSWORD_HISTORY, |
| | | "Display password history state values for the user"); |
| | | registerMessage(MSGID_DESCRIPTION_PWPSTATE_CLEAR_PASSWORD_HISTORY, |
| | | "Clear password history state values for the user. This " + |
| | | "should be used only for testing purposes"); |
| | | registerMessage(MSGID_PWPSTATE_MUTUALLY_EXCLUSIVE_ARGUMENTS, |
| | | "ERROR: You may not provide both the %s and the %s " + |
| | | "arguments"); |
| | |
| | | "Password Changed by Required Time"); |
| | | registerMessage(MSGID_PWPSTATE_LABEL_SECONDS_UNTIL_REQUIRED_CHANGE_TIME, |
| | | "Seconds Until Required Change Time"); |
| | | registerMessage(MSGID_PWPSTATE_LABEL_PASSWORD_HISTORY, |
| | | "Password History"); |
| | | registerMessage(MSGID_PWPSTATE_INVALID_RESPONSE_OP_TYPE, |
| | | "Unrecognized or invalid operation type: %s"); |
| | | } |
| | |
| | | |
| | | |
| | | /** |
| | | * The name of the subcommand that will be used to get the password history |
| | | * state values. |
| | | */ |
| | | private static final String SC_GET_PASSWORD_HISTORY = "get-password-history"; |
| | | |
| | | |
| | | |
| | | /** |
| | | * The name of the subcommand that will be used to clear the password history |
| | | * state values. |
| | | */ |
| | | private static final String SC_CLEAR_PASSWORD_HISTORY = |
| | | "clear-password-history"; |
| | | |
| | | |
| | | |
| | | /** |
| | | * The name of the argument that will be used for holding the value(s) to use |
| | | * for the target operation. |
| | | */ |
| | |
| | | printLabelAndValues(msgID, opValues); |
| | | break; |
| | | |
| | | case OP_GET_PASSWORD_HISTORY: |
| | | msgID = MSGID_PWPSTATE_LABEL_PASSWORD_HISTORY; |
| | | printLabelAndValues(msgID, opValues); |
| | | break; |
| | | |
| | | default: |
| | | msgID = MSGID_PWPSTATE_INVALID_RESPONSE_OP_TYPE; |
| | | String message = getMessage(msgID, opType); |
| | |
| | | msgID = MSGID_DESCRIPTION_PWPSTATE_GET_SECONDS_UNTIL_REQUIRED_CHANGE_TIME; |
| | | new SubCommand(argParser, SC_GET_SECONDS_UNTIL_REQUIRED_CHANGE_TIME, |
| | | msgID); |
| | | |
| | | msgID = MSGID_DESCRIPTION_PWPSTATE_GET_PASSWORD_HISTORY; |
| | | new SubCommand(argParser, SC_GET_PASSWORD_HISTORY, msgID); |
| | | |
| | | msgID = MSGID_DESCRIPTION_PWPSTATE_CLEAR_PASSWORD_HISTORY; |
| | | sc = new SubCommand(argParser, SC_CLEAR_PASSWORD_HISTORY, msgID); |
| | | sc.setHidden(true); |
| | | } |
| | | catch (ArgumentException ae) |
| | | { |
| | |
| | | opElements.add(encode(OP_GET_SECONDS_UNTIL_REQUIRED_CHANGE_TIME, |
| | | NO_VALUE)); |
| | | } |
| | | else if (subCommandName.equals(SC_GET_PASSWORD_HISTORY)) |
| | | { |
| | | opElements.add(encode(OP_GET_PASSWORD_HISTORY, NO_VALUE)); |
| | | } |
| | | else if (subCommandName.equals(SC_CLEAR_PASSWORD_HISTORY)) |
| | | { |
| | | opElements.add(encode(OP_CLEAR_PASSWORD_HISTORY, NO_VALUE)); |
| | | } |
| | | else |
| | | { |
| | | msgID = MSGID_PWPSTATE_INVALID_SUBCOMMAND; |
| | |
| | | } |
| | | } |
| | | } |
| | | |
| | | |
| | | // If we should check the password history, then do so now. |
| | | if (pwPolicyState.maintainHistory()) |
| | | { |
| | | List<AttributeValue> newPasswords = localOp.getNewPasswords(); |
| | | if (newPasswords != null) |
| | | { |
| | | for (AttributeValue v : newPasswords) |
| | | { |
| | | if (pwPolicyState.isPasswordInHistory(v.getValue())) |
| | | { |
| | | if (selfChange || (! pwPolicyState.getPolicy(). |
| | | skipValidationForAdministrators())) |
| | | { |
| | | localOp.setResultCode(ResultCode.UNWILLING_TO_PERFORM); |
| | | |
| | | int msgID = MSGID_MODIFY_PW_IN_HISTORY; |
| | | localOp.appendErrorMessage(getMessage(msgID)); |
| | | break modifyProcessing; |
| | | } |
| | | } |
| | | } |
| | | |
| | | pwPolicyState.updatePasswordHistory(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | |
| | |
| | | |
| | | |
| | | /** |
| | | * Tests the Directory Server's password history maintenance capabilities |
| | | * using only the password history count configuration option. |
| | | * |
| | | * @throws Exception If an unexpected problem occurs. |
| | | */ |
| | | @Test() |
| | | public void testPasswordHistoryUsingCount() |
| | | throws Exception |
| | | { |
| | | TestCaseUtils.initializeTestBackend(true); |
| | | TestCaseUtils.addEntry( |
| | | "dn: uid=test.user,o=test", |
| | | "objectClass: top", |
| | | "objectClass: person", |
| | | "objectClass: organizationalPerson", |
| | | "objectClass: inetOrgPerson", |
| | | "uid: test.user", |
| | | "givenName: Test", |
| | | "sn: User", |
| | | "cn: Test User", |
| | | "userPassword: originalPassword", |
| | | "ds-privilege-name: bypass-acl"); |
| | | |
| | | // Make sure that before we enable history features we can re-use the |
| | | // current password. |
| | | String origPWPath = TestCaseUtils.createTempFile( |
| | | "dn: uid=test.user,o=test", |
| | | "changetype: modify", |
| | | "replace: userPassword", |
| | | "userPassword: originalPassword"); |
| | | |
| | | String[] args = |
| | | { |
| | | "-h", "127.0.0.1", |
| | | "-p", String.valueOf(TestCaseUtils.getServerLdapPort()), |
| | | "-D", "uid=test.user,o=test", |
| | | "-w", "originalPassword", |
| | | "-f", origPWPath |
| | | }; |
| | | |
| | | assertEquals(LDAPModify.mainModify(args, false, System.out, System.err), 0); |
| | | |
| | | TestCaseUtils.applyModifications( |
| | | "dn: cn=Default Password Policy,cn=Password Policies,cn=config", |
| | | "changetype: modify", |
| | | "replace: ds-cfg-password-history-count", |
| | | "ds-cfg-password-history-count: 3"); |
| | | |
| | | try |
| | | { |
| | | // Make sure that we cannot re-use the original password as a new |
| | | // password. |
| | | assertFalse(LDAPModify.mainModify(args, false, System.out, System.err) == |
| | | 0); |
| | | |
| | | |
| | | // Change the password three times. |
| | | String newPWsPath = TestCaseUtils.createTempFile( |
| | | "dn: uid=test.user,o=test", |
| | | "changetype: modify", |
| | | "replace: userPassword", |
| | | "userPassword: newPassword1", |
| | | "", |
| | | "dn: uid=test.user,o=test", |
| | | "changetype: modify", |
| | | "replace: userPassword", |
| | | "userPassword: newPassword2", |
| | | "", |
| | | "dn: uid=test.user,o=test", |
| | | "changetype: modify", |
| | | "replace: userPassword", |
| | | "userPassword: newPassword3"); |
| | | |
| | | args = new String[] |
| | | { |
| | | "-h", "127.0.0.1", |
| | | "-p", String.valueOf(TestCaseUtils.getServerLdapPort()), |
| | | "-D", "uid=test.user,o=test", |
| | | "-w", "originalPassword", |
| | | "-f", newPWsPath |
| | | }; |
| | | |
| | | assertEquals(LDAPModify.mainModify(args, false, System.out, System.err), |
| | | 0); |
| | | |
| | | |
| | | // Make sure that we still can't use the original password. |
| | | args = new String[] |
| | | { |
| | | "-h", "127.0.0.1", |
| | | "-p", String.valueOf(TestCaseUtils.getServerLdapPort()), |
| | | "-D", "uid=test.user,o=test", |
| | | "-w", "newPassword3", |
| | | "-f", origPWPath |
| | | }; |
| | | |
| | | assertFalse(LDAPModify.mainModify(args, false, System.out, System.err) == |
| | | 0); |
| | | |
| | | |
| | | // Change the password one more time and then verify that we can use the |
| | | // original password again. |
| | | String newPWsPath2 = TestCaseUtils.createTempFile( |
| | | "dn: uid=test.user,o=test", |
| | | "changetype: modify", |
| | | "replace: userPassword", |
| | | "userPassword: newPassword4", |
| | | "", |
| | | "dn: uid=test.user,o=test", |
| | | "changetype: modify", |
| | | "replace: userPassword", |
| | | "userPassword: originalPassword"); |
| | | |
| | | args = new String[] |
| | | { |
| | | "-h", "127.0.0.1", |
| | | "-p", String.valueOf(TestCaseUtils.getServerLdapPort()), |
| | | "-D", "uid=test.user,o=test", |
| | | "-w", "newPassword3", |
| | | "-f", newPWsPath2 |
| | | }; |
| | | |
| | | assertEquals(LDAPModify.mainModify(args, false, System.out, System.err), |
| | | 0); |
| | | |
| | | |
| | | // Make sure that we can't use the first new password. |
| | | String firstPWPath = TestCaseUtils.createTempFile( |
| | | "dn: uid=test.user,o=test", |
| | | "changetype: modify", |
| | | "replace: userPassword", |
| | | "userPassword: newPassword1"); |
| | | |
| | | args = new String[] |
| | | { |
| | | "-h", "127.0.0.1", |
| | | "-p", String.valueOf(TestCaseUtils.getServerLdapPort()), |
| | | "-D", "uid=test.user,o=test", |
| | | "-w", "originalPassword", |
| | | "-f", firstPWPath |
| | | }; |
| | | |
| | | assertFalse(LDAPModify.mainModify(args, false, System.out, System.err) == |
| | | 0); |
| | | |
| | | |
| | | // Reduce the password history count from 3 to 2 and verify that we can |
| | | // now use the first new password. |
| | | TestCaseUtils.applyModifications( |
| | | "dn: cn=Default Password Policy,cn=Password Policies,cn=config", |
| | | "changetype: modify", |
| | | "replace: ds-cfg-password-history-count", |
| | | "ds-cfg-password-history-count: 0"); |
| | | |
| | | assertEquals(LDAPModify.mainModify(args, false, System.out, System.err), |
| | | 0); |
| | | } |
| | | finally |
| | | { |
| | | TestCaseUtils.applyModifications( |
| | | "dn: cn=Default Password Policy,cn=Password Policies,cn=config", |
| | | "changetype: modify", |
| | | "replace: ds-cfg-password-history-count", |
| | | "ds-cfg-password-history-count: 0"); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Tests the Directory Server's password history maintenance capabilities |
| | | * using only the password history duration configuration option. |
| | | * |
| | | * @throws Exception If an unexpected problem occurs. |
| | | */ |
| | | @Test(groups = "slow") |
| | | public void testPasswordHistoryUsingDuration() |
| | | throws Exception |
| | | { |
| | | TestCaseUtils.initializeTestBackend(true); |
| | | TestCaseUtils.addEntry( |
| | | "dn: uid=test.user,o=test", |
| | | "objectClass: top", |
| | | "objectClass: person", |
| | | "objectClass: organizationalPerson", |
| | | "objectClass: inetOrgPerson", |
| | | "uid: test.user", |
| | | "givenName: Test", |
| | | "sn: User", |
| | | "cn: Test User", |
| | | "userPassword: originalPassword", |
| | | "ds-privilege-name: bypass-acl"); |
| | | |
| | | // Make sure that before we enable history features we can re-use the |
| | | // current password. |
| | | String origPWPath = TestCaseUtils.createTempFile( |
| | | "dn: uid=test.user,o=test", |
| | | "changetype: modify", |
| | | "replace: userPassword", |
| | | "userPassword: originalPassword"); |
| | | |
| | | String[] args = |
| | | { |
| | | "-h", "127.0.0.1", |
| | | "-p", String.valueOf(TestCaseUtils.getServerLdapPort()), |
| | | "-D", "uid=test.user,o=test", |
| | | "-w", "originalPassword", |
| | | "-f", origPWPath |
| | | }; |
| | | |
| | | assertEquals(LDAPModify.mainModify(args, false, System.out, System.err), 0); |
| | | |
| | | TestCaseUtils.applyModifications( |
| | | "dn: cn=Default Password Policy,cn=Password Policies,cn=config", |
| | | "changetype: modify", |
| | | "replace: ds-cfg-password-history-duration", |
| | | "ds-cfg-password-history-duration: 5 seconds"); |
| | | |
| | | try |
| | | { |
| | | // Make sure that we can no longer re-use the original password as a new |
| | | // password. |
| | | assertFalse(LDAPModify.mainModify(args, false, System.out, System.err) == |
| | | 0); |
| | | |
| | | |
| | | // Change the password three times. |
| | | String newPWsPath = TestCaseUtils.createTempFile( |
| | | "dn: uid=test.user,o=test", |
| | | "changetype: modify", |
| | | "replace: userPassword", |
| | | "userPassword: newPassword1", |
| | | "", |
| | | "dn: uid=test.user,o=test", |
| | | "changetype: modify", |
| | | "replace: userPassword", |
| | | "userPassword: newPassword2", |
| | | "", |
| | | "dn: uid=test.user,o=test", |
| | | "changetype: modify", |
| | | "replace: userPassword", |
| | | "userPassword: newPassword3"); |
| | | |
| | | args = new String[] |
| | | { |
| | | "-h", "127.0.0.1", |
| | | "-p", String.valueOf(TestCaseUtils.getServerLdapPort()), |
| | | "-D", "uid=test.user,o=test", |
| | | "-w", "originalPassword", |
| | | "-f", newPWsPath |
| | | }; |
| | | |
| | | assertEquals(LDAPModify.mainModify(args, false, System.out, System.err), |
| | | 0); |
| | | |
| | | |
| | | // Make sure that we still can't use the original password. |
| | | args = new String[] |
| | | { |
| | | "-h", "127.0.0.1", |
| | | "-p", String.valueOf(TestCaseUtils.getServerLdapPort()), |
| | | "-D", "uid=test.user,o=test", |
| | | "-w", "newPassword3", |
| | | "-f", origPWPath |
| | | }; |
| | | |
| | | assertFalse(LDAPModify.mainModify(args, false, System.out, System.err) == |
| | | 0); |
| | | |
| | | |
| | | // Sleep for six seconds and then verify that we can use the original |
| | | // password again. |
| | | Thread.sleep(6000); |
| | | assertEquals(LDAPModify.mainModify(args, false, System.out, System.err), |
| | | 0); |
| | | } |
| | | finally |
| | | { |
| | | TestCaseUtils.applyModifications( |
| | | "dn: cn=Default Password Policy,cn=Password Policies,cn=config", |
| | | "changetype: modify", |
| | | "replace: ds-cfg-password-history-duration", |
| | | "ds-cfg-password-history-duration: 0 seconds"); |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | /** |
| | | * Tests the <CODE>toString</CODE> methods with the default password policy. |
| | | */ |
| | | @Test() |
| | |
| | | new Object[] { "get-password-changed-by-required-time" }, |
| | | new Object[] { "set-password-changed-by-required-time" }, |
| | | new Object[] { "clear-password-changed-by-required-time" }, |
| | | new Object[] { "get-seconds-until-required-change-time" } |
| | | new Object[] { "get-seconds-until-required-change-time" }, |
| | | new Object[] { "get-password-history" }, |
| | | new Object[] { "clear-password-history" } |
| | | }; |
| | | } |
| | | |
| | |
| | | new Object[] { "get-grace-login-use-times" }, |
| | | new Object[] { "get-remaining-grace-login-count" }, |
| | | new Object[] { "get-password-changed-by-required-time" }, |
| | | new Object[] { "get-seconds-until-required-change-time" } |
| | | new Object[] { "get-seconds-until-required-change-time" }, |
| | | new Object[] { "get-password-history" } |
| | | }; |
| | | } |
| | | |
| | |
| | | new Object[] { "clear-last-login-time" }, |
| | | new Object[] { "clear-password-is-reset" }, |
| | | new Object[] { "clear-grace-login-use-times" }, |
| | | new Object[] { "clear-password-changed-by-required-time" } |
| | | new Object[] { "clear-password-changed-by-required-time" }, |
| | | new Object[] { "clear-password-history" } |
| | | }; |
| | | } |
| | | |