From 2c7b8d6d8c0c177e8089272140dae66b87852ff7 Mon Sep 17 00:00:00 2001
From: neil_a_wilson <neil_a_wilson@localhost>
Date: Tue, 17 Jul 2007 21:59:32 +0000
Subject: [PATCH] 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.
---
opends/resource/schema/02-config.ldif | 9
opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendWorkflowElement.java | 27 +
opends/src/server/org/opends/server/extensions/PasswordPolicyStateExtendedOperation.java | 31 +
opends/tests/unit-tests-testng/src/server/org/opends/server/core/PasswordPolicyTestCase.java | 289 +++++++++++++
opends/resource/config/config.ldif | 4
opends/src/server/org/opends/server/messages/CoreMessages.java | 12
opends/src/server/org/opends/server/messages/ExtensionsMessages.java | 13
opends/src/server/org/opends/server/extensions/PasswordModifyExtendedOperation.java | 28 +
opends/src/server/org/opends/server/messages/ToolMessages.java | 34 +
opends/src/server/org/opends/server/tools/ManageAccount.java | 37 +
opends/src/server/org/opends/server/core/PasswordPolicyState.java | 621 +++++++++++++++++++++++++++++
opends/src/admin/defn/org/opends/server/admin/std/PasswordPolicyConfiguration.xml | 59 ++
opends/src/server/org/opends/server/core/PasswordPolicy.java | 47 ++
opends/tests/unit-tests-testng/src/server/org/opends/server/tools/ManageAccountTestCase.java | 10
opends/src/server/org/opends/server/config/ConfigConstants.java | 31 +
15 files changed, 1,247 insertions(+), 5 deletions(-)
diff --git a/opends/resource/config/config.ldif b/opends/resource/config/config.ldif
index 61302b1..003d635 100644
--- a/opends/resource/config/config.ldif
+++ b/opends/resource/config/config.ldif
@@ -1080,6 +1080,8 @@
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
@@ -1108,6 +1110,8 @@
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
diff --git a/opends/resource/schema/02-config.ldif b/opends/resource/schema/02-config.ldif
index 00341bd..82bba2c 100644
--- a/opends/resource/schema/02-config.ldif
+++ b/opends/resource/schema/02-config.ldif
@@ -1478,6 +1478,12 @@
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 )
@@ -1807,7 +1813,8 @@
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 $
diff --git a/opends/src/admin/defn/org/opends/server/admin/std/PasswordPolicyConfiguration.xml b/opends/src/admin/defn/org/opends/server/admin/std/PasswordPolicyConfiguration.xml
index 933f525..447b814 100644
--- a/opends/src/admin/defn/org/opends/server/admin/std/PasswordPolicyConfiguration.xml
+++ b/opends/src/admin/defn/org/opends/server/admin/std/PasswordPolicyConfiguration.xml
@@ -946,7 +946,7 @@
</adm:property>
<adm:property name="state-update-failure-policy" mandatory="false"
- multi-valued="false">
+ multi-valued="false">
<adm:synopsis>
Specifies how the server should deal with the inability to update password
policy state information during an authentication attempt. In particular,
@@ -996,5 +996,62 @@
</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>
+
</adm:managed-object>
diff --git a/opends/src/server/org/opends/server/config/ConfigConstants.java b/opends/src/server/org/opends/server/config/ConfigConstants.java
index ba2e5c2..818e728 100644
--- a/opends/src/server/org/opends/server/config/ConfigConstants.java
+++ b/opends/src/server/org/opends/server/config/ConfigConstants.java
@@ -1802,6 +1802,21 @@
/**
+ * 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.
*/
@@ -3449,6 +3464,22 @@
/**
+ * 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.
*/
diff --git a/opends/src/server/org/opends/server/core/PasswordPolicy.java b/opends/src/server/org/opends/server/core/PasswordPolicy.java
index 6e5eb90..37e40a4 100644
--- a/opends/src/server/org/opends/server/core/PasswordPolicy.java
+++ b/opends/src/server/org/opends/server/core/PasswordPolicy.java
@@ -175,6 +175,12 @@
// 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;
@@ -812,6 +818,11 @@
this.stateUpdateFailurePolicy = configuration.getStateUpdateFailurePolicy();
+ // Get the password history count and duration.
+ this.historyCount = configuration.getPasswordHistoryCount();
+ this.historyDuration = (int) configuration.getPasswordHistoryDuration();
+
+
/*
* Holistic validation.
*/
@@ -1115,6 +1126,34 @@
/**
+ * 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.
*
@@ -1739,6 +1778,14 @@
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);
}
}
diff --git a/opends/src/server/org/opends/server/core/PasswordPolicyState.java b/opends/src/server/org/opends/server/core/PasswordPolicyState.java
index bd5be3f..a73bc74 100644
--- a/opends/src/server/org/opends/server/core/PasswordPolicyState.java
+++ b/opends/src/server/org/opends/server/core/PasswordPolicyState.java
@@ -38,6 +38,7 @@
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;
@@ -77,6 +78,7 @@
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.*;
@@ -3952,6 +3954,625 @@
/**
+ * 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
diff --git a/opends/src/server/org/opends/server/extensions/PasswordModifyExtendedOperation.java b/opends/src/server/org/opends/server/extensions/PasswordModifyExtendedOperation.java
index 47bfac2..2676af9 100644
--- a/opends/src/server/org/opends/server/extensions/PasswordModifyExtendedOperation.java
+++ b/opends/src/server/org/opends/server/extensions/PasswordModifyExtendedOperation.java
@@ -861,6 +861,7 @@
}
else
{
+ // Run the new password through the set of password validators.
if (selfChange ||
(! pwPolicyState.getPolicy().skipValidationForAdministrators()))
{
@@ -919,6 +920,33 @@
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();
+ }
+ }
}
}
diff --git a/opends/src/server/org/opends/server/extensions/PasswordPolicyStateExtendedOperation.java b/opends/src/server/org/opends/server/extensions/PasswordPolicyStateExtendedOperation.java
index a8c6deb..297b64a 100644
--- a/opends/src/server/org/opends/server/extensions/PasswordPolicyStateExtendedOperation.java
+++ b/opends/src/server/org/opends/server/extensions/PasswordPolicyStateExtendedOperation.java
@@ -121,6 +121,8 @@
* setPasswordChangedByRequiredTime (36),
* clearPasswordChangedByRequiredTime (37),
* getSecondsUntilRequiredChangeTime (38),
+ * getPasswordHistory (39),
+ * clearPasswordHistory (40),
* ... },
* opValues SEQUENCE OF OCTET STRING OPTIONAL }
* </PRE>
@@ -420,6 +422,20 @@
+ /**
+ * 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;
@@ -1225,6 +1241,15 @@
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));
@@ -1604,6 +1629,12 @@
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);
diff --git a/opends/src/server/org/opends/server/messages/CoreMessages.java b/opends/src/server/org/opends/server/messages/CoreMessages.java
index 470fb15..1c0331c 100644
--- a/opends/src/server/org/opends/server/messages/CoreMessages.java
+++ b/opends/src/server/org/opends/server/messages/CoreMessages.java
@@ -6269,6 +6269,15 @@
/**
+ * 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.
*/
@@ -7504,6 +7513,9 @@
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 " +
diff --git a/opends/src/server/org/opends/server/messages/ExtensionsMessages.java b/opends/src/server/org/opends/server/messages/ExtensionsMessages.java
index 36d4a23..e4006f6 100644
--- a/opends/src/server/org/opends/server/messages/ExtensionsMessages.java
+++ b/opends/src/server/org/opends/server/messages/ExtensionsMessages.java
@@ -5502,6 +5502,16 @@
/**
+ * 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.
*/
@@ -5786,6 +5796,9 @@
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");
diff --git a/opends/src/server/org/opends/server/messages/ToolMessages.java b/opends/src/server/org/opends/server/messages/ToolMessages.java
index 40cd98d..5fcaf8b 100644
--- a/opends/src/server/org/opends/server/messages/ToolMessages.java
+++ b/opends/src/server/org/opends/server/messages/ToolMessages.java
@@ -9036,6 +9036,33 @@
/**
+ * 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.
*/
@@ -11867,6 +11894,11 @@
"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");
@@ -11951,6 +11983,8 @@
"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");
}
diff --git a/opends/src/server/org/opends/server/tools/ManageAccount.java b/opends/src/server/org/opends/server/tools/ManageAccount.java
index 7c9b52d..9ab38c7 100644
--- a/opends/src/server/org/opends/server/tools/ManageAccount.java
+++ b/opends/src/server/org/opends/server/tools/ManageAccount.java
@@ -441,6 +441,23 @@
/**
+ * 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.
*/
@@ -760,6 +777,11 @@
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);
@@ -1130,6 +1152,13 @@
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)
{
@@ -1645,6 +1674,14 @@
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;
diff --git a/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendWorkflowElement.java b/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendWorkflowElement.java
index 2012bee..f440150 100644
--- a/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendWorkflowElement.java
+++ b/opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendWorkflowElement.java
@@ -1869,6 +1869,33 @@
}
}
}
+
+
+ // 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();
+ }
+ }
}
diff --git a/opends/tests/unit-tests-testng/src/server/org/opends/server/core/PasswordPolicyTestCase.java b/opends/tests/unit-tests-testng/src/server/org/opends/server/core/PasswordPolicyTestCase.java
index 6384d01..85d6569 100644
--- a/opends/tests/unit-tests-testng/src/server/org/opends/server/core/PasswordPolicyTestCase.java
+++ b/opends/tests/unit-tests-testng/src/server/org/opends/server/core/PasswordPolicyTestCase.java
@@ -4822,6 +4822,295 @@
/**
+ * 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()
diff --git a/opends/tests/unit-tests-testng/src/server/org/opends/server/tools/ManageAccountTestCase.java b/opends/tests/unit-tests-testng/src/server/org/opends/server/tools/ManageAccountTestCase.java
index 0ca38f2..8635359 100644
--- a/opends/tests/unit-tests-testng/src/server/org/opends/server/tools/ManageAccountTestCase.java
+++ b/opends/tests/unit-tests-testng/src/server/org/opends/server/tools/ManageAccountTestCase.java
@@ -110,7 +110,9 @@
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" }
};
}
@@ -147,7 +149,8 @@
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" }
};
}
@@ -219,7 +222,8 @@
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" }
};
}
--
Gitblit v1.10.0