From 2c1f6b616b0466880bb023803225f17c687adccf Mon Sep 17 00:00:00 2001
From: neil_a_wilson <neil_a_wilson@localhost>
Date: Mon, 26 Mar 2007 22:00:15 +0000
Subject: [PATCH] Add two new password validators to OpenDS:

---
 opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/RepeatedCharactersPasswordValidatorTestCase.java |  699 ++++++++++++++++++++
 opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/UniqueCharactersPasswordValidatorTestCase.java   |  699 ++++++++++++++++++++
 opendj-sdk/opends/src/server/org/opends/server/extensions/RepeatedCharactersPasswordValidator.java                                 |  196 +++++
 opendj-sdk/opends/src/admin/defn/org/opends/server/admin/std/RepeatedCharactersPasswordValidatorConfiguration.xml                  |   66 +
 opendj-sdk/opends/src/admin/defn/org/opends/server/admin/std/UniqueCharactersPasswordValidatorConfiguration.xml                    |   64 +
 opendj-sdk/opends/resource/config/config.ldif                                                                                      |   20 
 opendj-sdk/opends/src/server/org/opends/server/messages/ExtensionsMessages.java                                                    |   32 
 opendj-sdk/opends/resource/schema/02-config.ldif                                                                                   |   22 
 opendj-sdk/opends/src/server/org/opends/server/extensions/UniqueCharactersPasswordValidator.java                                   |  187 +++++
 9 files changed, 1,985 insertions(+), 0 deletions(-)

diff --git a/opendj-sdk/opends/resource/config/config.ldif b/opendj-sdk/opends/resource/config/config.ldif
index 4679b65..6c192c1 100644
--- a/opendj-sdk/opends/resource/config/config.ldif
+++ b/opendj-sdk/opends/resource/config/config.ldif
@@ -1098,6 +1098,16 @@
 ds-cfg-minimum-password-length: 6
 ds-cfg-maximum-password-length: 0
 
+dn: cn=Repeated Characters,cn=Password Validators,cn=config
+objectClass: top
+objectClass: ds-cfg-password-validator
+objectClass: ds-cfg-repeated-characters-password-validator
+cn: Repeated Characters
+ds-cfg-password-validator-class: org.opends.server.extensions.RepeatedCharactersPasswordValidator
+ds-cfg-password-validator-enabled: true
+ds-cfg-maximum-consecutive-length: 2
+ds-cfg-case-sensitive-validation: false
+
 dn: cn=Similarity-Based Password Validator,cn=Password Validators,cn=config
 objectClass: top
 objectClass: ds-cfg-password-validator
@@ -1107,6 +1117,16 @@
 ds-cfg-password-validator-enabled: true
 ds-cfg-minimum-password-difference: 3
 
+dn: cn=Unique Characters,cn=Password Validators,cn=config
+objectClass: top
+objectClass: ds-cfg-password-validator
+objectClass: ds-cfg-unique-characters-password-validator
+cn: Unique Characters
+ds-cfg-password-validator-class: org.opends.server.extensions.UniqueCharactersPasswordValidator
+ds-cfg-password-validator-enabled: true
+ds-cfg-minimum-unique-characters: 5
+ds-cfg-case-sensitive-validation: false
+
 dn: cn=Plugins,cn=config
 objectClass: top
 objectClass: ds-cfg-branch
diff --git a/opendj-sdk/opends/resource/schema/02-config.ldif b/opendj-sdk/opends/resource/schema/02-config.ldif
index c6e712e..b5791ca 100644
--- a/opendj-sdk/opends/resource/schema/02-config.ldif
+++ b/opendj-sdk/opends/resource/schema/02-config.ldif
@@ -1091,6 +1091,18 @@
   NAME 'ds-cfg-minimum-password-difference' 
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE 
   X-ORIGIN 'OpenDS Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.26027.1.1.322
+  NAME 'ds-cfg-minimum-unique-characters' 
+  SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE 
+  X-ORIGIN 'OpenDS Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.26027.1.1.323
+  NAME 'ds-cfg-maximum-consecutive-length' 
+  SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE 
+  X-ORIGIN 'OpenDS Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.26027.1.1.324
+  NAME 'ds-cfg-case-sensitive-validation' 
+  SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE 
+  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 )
@@ -1510,4 +1522,14 @@
   NAME 'ds-cfg-similarity-based-password-validator'
   SUP ds-cfg-password-validator STRUCTURAL
   MUST ds-cfg-minimum-password-difference X-ORIGIN 'OpenDS Directory Server' )
+objectClasses: ( 1.3.6.1.4.1.26027.1.2.89
+  NAME 'ds-cfg-unique-characters-password-validator'
+  SUP ds-cfg-password-validator STRUCTURAL
+  MUST ( ds-cfg-minimum-unique-characters $ ds-cfg-case-sensitive-validation )
+  X-ORIGIN 'OpenDS Directory Server' )
+objectClasses: ( 1.3.6.1.4.1.26027.1.2.90
+  NAME 'ds-cfg-repeated-characters-password-validator'
+  SUP ds-cfg-password-validator STRUCTURAL
+  MUST ( ds-cfg-maximum-consecutive-length $ ds-cfg-case-sensitive-validation )
+  X-ORIGIN 'OpenDS Directory Server' )
 
diff --git a/opendj-sdk/opends/src/admin/defn/org/opends/server/admin/std/RepeatedCharactersPasswordValidatorConfiguration.xml b/opendj-sdk/opends/src/admin/defn/org/opends/server/admin/std/RepeatedCharactersPasswordValidatorConfiguration.xml
new file mode 100644
index 0000000..86c3fb9
--- /dev/null
+++ b/opendj-sdk/opends/src/admin/defn/org/opends/server/admin/std/RepeatedCharactersPasswordValidatorConfiguration.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<adm:managed-object name="repeated-characters-password-validator"
+plural-name="repeated-characters-password-validators"
+package="org.opends.server.admin.std" extends="password-validator"
+xmlns:adm="http://www.opends.org/admin"
+xmlns:ldap="http://www.opends.org/admin-ldap">
+  <adm:synopsis>
+    The
+    <adm:user-friendly-name />
+    is used to determine whether a proposed password is acceptable based on
+    the number of times any character may appear consecutively in a password
+    value.
+  </adm:synopsis>
+  <adm:profile name="ldap">
+    <ldap:object-class>
+      <ldap:oid>1.3.6.1.4.1.26027.1.2.90</ldap:oid>
+      <ldap:name>ds-cfg-repeated-characters-password-validator</ldap:name>
+      <ldap:superior>ds-cfg-password-validator</ldap:superior>
+    </ldap:object-class>
+  </adm:profile>
+  <adm:property name="maximum-consecutive-length" mandatory="true">
+    <adm:synopsis>
+      Specifies the maximum number of times that any character may appear
+      consecutively in a password value.
+    </adm:synopsis>
+    <adm:description>
+      Specifies the maximum number of times that any character may appear
+      consecutively in a password value.  A value of zero indicates that there
+      will be no maximum limit enforced.  Changes to this configuration
+      attribute will take effect immediately.
+    </adm:description>
+    <adm:syntax>
+      <adm:integer lower-limit="0" />
+    </adm:syntax>
+    <adm:profile name="ldap">
+      <ldap:attribute>
+        <ldap:oid>1.3.6.1.4.1.26027.1.1.323</ldap:oid>
+        <ldap:name>ds-cfg-maximum-consecutive-length</ldap:name>
+      </ldap:attribute>
+    </adm:profile>
+  </adm:property>
+  <adm:property name="case-sensitive-validation" mandatory="true">
+    <adm:synopsis>
+      Indicates whether this password validator should treat password characters
+      in a case-sensitive manner.
+    </adm:synopsis>
+    <adm:description>
+      Indicates whether this password validator should treat password characters
+      in a case-sensitive manner.  A value of false indicates that any
+      differences in capitalization should be ignored when looking for
+      consecutive characters in the password.  A value of true indicates that
+      a character should only be considered repeating if all consecutive
+      occurrences use the same capitalization.
+    </adm:description>
+    <adm:syntax>
+      <adm:boolean />
+    </adm:syntax>
+    <adm:profile name="ldap">
+      <ldap:attribute>
+        <ldap:oid>1.3.6.1.4.1.26027.1.1.324</ldap:oid>
+        <ldap:name>ds-cfg-case-sensitive-validation</ldap:name>
+      </ldap:attribute>
+    </adm:profile>
+  </adm:property>
+</adm:managed-object>
+
diff --git a/opendj-sdk/opends/src/admin/defn/org/opends/server/admin/std/UniqueCharactersPasswordValidatorConfiguration.xml b/opendj-sdk/opends/src/admin/defn/org/opends/server/admin/std/UniqueCharactersPasswordValidatorConfiguration.xml
new file mode 100644
index 0000000..18b0586
--- /dev/null
+++ b/opendj-sdk/opends/src/admin/defn/org/opends/server/admin/std/UniqueCharactersPasswordValidatorConfiguration.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<adm:managed-object name="unique-characters-password-validator"
+plural-name="unique-characters-password-validators"
+package="org.opends.server.admin.std" extends="password-validator"
+xmlns:adm="http://www.opends.org/admin"
+xmlns:ldap="http://www.opends.org/admin-ldap">
+  <adm:synopsis>
+    The
+    <adm:user-friendly-name />
+    is used to determine whether a proposed password is acceptable based on
+    the number of unique characters that it contains.
+  </adm:synopsis>
+  <adm:profile name="ldap">
+    <ldap:object-class>
+      <ldap:oid>1.3.6.1.4.1.26027.1.2.89</ldap:oid>
+      <ldap:name>ds-cfg-unique-characters-password-validator</ldap:name>
+      <ldap:superior>ds-cfg-password-validator</ldap:superior>
+    </ldap:object-class>
+  </adm:profile>
+  <adm:property name="minimum-unique-characters" mandatory="true">
+    <adm:synopsis>
+      Specifies the minimum number of unique characters that a password will be
+      allowed to contain.
+    </adm:synopsis>
+    <adm:description>
+      Specifies the minimum number of unique characters that a password will be
+      allowed to contain.  A value of zero indicates that there will be no
+      minimum value enforced.  Changes to this configuration attribute will take
+      effect immediately.
+    </adm:description>
+    <adm:syntax>
+      <adm:integer lower-limit="0" />
+    </adm:syntax>
+    <adm:profile name="ldap">
+      <ldap:attribute>
+        <ldap:oid>1.3.6.1.4.1.26027.1.1.322</ldap:oid>
+        <ldap:name>ds-cfg-minimum-unique-characters</ldap:name>
+      </ldap:attribute>
+    </adm:profile>
+  </adm:property>
+  <adm:property name="case-sensitive-validation" mandatory="true">
+    <adm:synopsis>
+      Indicates whether this password validator should treat password characters
+      in a case-sensitive manner.
+    </adm:synopsis>
+    <adm:description>
+      Indicates whether this password validator should treat password characters
+      in a case-sensitive manner.  A value of true indicates that a capital
+      letter should not be considered the same as its lowercase counterpart.
+      A value of false indicates that differences in capitalization should be
+      ignored when looking at the number of unique characters in the password.
+    </adm:description>
+    <adm:syntax>
+      <adm:boolean />
+    </adm:syntax>
+    <adm:profile name="ldap">
+      <ldap:attribute>
+        <ldap:oid>1.3.6.1.4.1.26027.1.1.324</ldap:oid>
+        <ldap:name>ds-cfg-case-sensitive-validation</ldap:name>
+      </ldap:attribute>
+    </adm:profile>
+  </adm:property>
+</adm:managed-object>
+
diff --git a/opendj-sdk/opends/src/server/org/opends/server/extensions/RepeatedCharactersPasswordValidator.java b/opendj-sdk/opends/src/server/org/opends/server/extensions/RepeatedCharactersPasswordValidator.java
new file mode 100644
index 0000000..0811659
--- /dev/null
+++ b/opendj-sdk/opends/src/server/org/opends/server/extensions/RepeatedCharactersPasswordValidator.java
@@ -0,0 +1,196 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License").  You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE
+ * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
+ * add the following below this CDDL HEADER, with the fields enclosed
+ * by brackets "[]" replaced with your own identifying information:
+ *      Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ *
+ *      Portions Copyright 2007 Sun Microsystems, Inc.
+ */
+package org.opends.server.extensions;
+
+
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import org.opends.server.admin.server.ConfigurationChangeListener;
+import org.opends.server.admin.std.server.
+            RepeatedCharactersPasswordValidatorCfg;
+import org.opends.server.api.PasswordValidator;
+import org.opends.server.core.Operation;
+import org.opends.server.types.ConfigChangeResult;
+import org.opends.server.types.ByteString;
+import org.opends.server.types.Entry;
+import org.opends.server.types.ResultCode;
+
+import static org.opends.server.messages.ExtensionsMessages.*;
+import static org.opends.server.messages.MessageHandler.*;
+
+
+
+/**
+ * This class provides an OpenDS password validator that may be used to ensure
+ * that proposed passwords are not allowed to have the same character appear
+ * several times consecutively.
+ */
+public class RepeatedCharactersPasswordValidator
+       extends PasswordValidator<RepeatedCharactersPasswordValidatorCfg>
+       implements ConfigurationChangeListener<
+                       RepeatedCharactersPasswordValidatorCfg>
+{
+  // The current configuration for this password validator.
+  private RepeatedCharactersPasswordValidatorCfg currentConfig;
+
+
+
+  /**
+   * Creates a new instance of this repeated characters password validator.
+   */
+  public RepeatedCharactersPasswordValidator()
+  {
+    super();
+
+    // No implementation is required here.  All initialization should be
+    // performed in the initializePasswordValidator() method.
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void initializePasswordValidator(
+                   RepeatedCharactersPasswordValidatorCfg configuration)
+  {
+    configuration.addRepeatedCharactersChangeListener(this);
+    currentConfig = configuration;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void finalizePasswordValidator()
+  {
+    currentConfig.removeRepeatedCharactersChangeListener(this);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public boolean passwordIsAcceptable(ByteString newPassword,
+                                      Set<ByteString> currentPasswords,
+                                      Operation operation, Entry userEntry,
+                                      StringBuilder invalidReason)
+  {
+    // Get a handle to the current configuration and see if we need to count
+    // the number of repeated characters in the password.
+    RepeatedCharactersPasswordValidatorCfg config = currentConfig;
+    int maxRepeats = config.getMaximumConsecutiveLength();
+    if (maxRepeats <= 0)
+    {
+      // We don't need to check anything, so the password will be acceptable.
+      return true;
+    }
+
+
+    // Get the password as a string.  If we should use case-insensitive
+    // validation, then convert it to use all lowercase characters.
+    String passwordString = newPassword.stringValue();
+    if (! config.isCaseSensitiveValidation())
+    {
+      passwordString = passwordString.toLowerCase();
+    }
+
+
+    // Create variables to keep track of the last character we've seen and how
+    // many times we have seen it.
+    char lastCharacter    = '\u0000';
+    int  consecutiveCount = 0;
+
+
+    // Iterate through the characters in the password.  If the consecutive
+    // count ever gets too high, then fail.
+    for (int i=0; i < passwordString.length(); i++)
+    {
+      char currentCharacter = passwordString.charAt(i);
+      if (currentCharacter == lastCharacter)
+      {
+        consecutiveCount++;
+        if (consecutiveCount > maxRepeats)
+        {
+          int    msgID   = MSGID_REPEATEDCHARS_VALIDATOR_TOO_MANY_CONSECUTIVE;
+          String message = getMessage(msgID, maxRepeats);
+          invalidReason.append(message);
+          return false;
+        }
+      }
+      else
+      {
+        lastCharacter    = currentCharacter;
+        consecutiveCount = 1;
+      }
+    }
+
+    return true;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  public boolean isConfigurationChangeAcceptable(
+                      RepeatedCharactersPasswordValidatorCfg configuration,
+                      List<String> unacceptableReasons)
+  {
+    // All of the necessary validation should have been performed automatically,
+    // so if we get to this point then the new configuration will be acceptable.
+    return true;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  public ConfigChangeResult applyConfigurationChange(
+                      RepeatedCharactersPasswordValidatorCfg configuration)
+  {
+    ResultCode        resultCode          = ResultCode.SUCCESS;
+    boolean           adminActionRequired = false;
+    ArrayList<String> messages            = new ArrayList<String>();
+
+    // For this password validator, we will always be able to successfully apply
+    // the new configuration.
+    currentConfig = configuration;
+
+    return new ConfigChangeResult(resultCode, adminActionRequired, messages);
+  }
+}
+
diff --git a/opendj-sdk/opends/src/server/org/opends/server/extensions/UniqueCharactersPasswordValidator.java b/opendj-sdk/opends/src/server/org/opends/server/extensions/UniqueCharactersPasswordValidator.java
new file mode 100644
index 0000000..a734c6c
--- /dev/null
+++ b/opendj-sdk/opends/src/server/org/opends/server/extensions/UniqueCharactersPasswordValidator.java
@@ -0,0 +1,187 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License").  You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE
+ * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
+ * add the following below this CDDL HEADER, with the fields enclosed
+ * by brackets "[]" replaced with your own identifying information:
+ *      Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ *
+ *      Portions Copyright 2007 Sun Microsystems, Inc.
+ */
+package org.opends.server.extensions;
+
+
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.opends.server.admin.server.ConfigurationChangeListener;
+import org.opends.server.admin.std.server.UniqueCharactersPasswordValidatorCfg;
+import org.opends.server.api.PasswordValidator;
+import org.opends.server.core.Operation;
+import org.opends.server.types.ConfigChangeResult;
+import org.opends.server.types.ByteString;
+import org.opends.server.types.Entry;
+import org.opends.server.types.ResultCode;
+
+import static org.opends.server.messages.ExtensionsMessages.*;
+import static org.opends.server.messages.MessageHandler.*;
+
+
+
+/**
+ * This class provides an OpenDS password validator that may be used to ensure
+ * that proposed passwords contain at least a specified number of different
+ * characters.
+ */
+public class UniqueCharactersPasswordValidator
+       extends PasswordValidator<UniqueCharactersPasswordValidatorCfg>
+       implements ConfigurationChangeListener<
+                       UniqueCharactersPasswordValidatorCfg>
+{
+  // The current configuration for this password validator.
+  private UniqueCharactersPasswordValidatorCfg currentConfig;
+
+
+
+  /**
+   * Creates a new instance of this unique characters password validator.
+   */
+  public UniqueCharactersPasswordValidator()
+  {
+    super();
+
+    // No implementation is required here.  All initialization should be
+    // performed in the initializePasswordValidator() method.
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void initializePasswordValidator(
+                   UniqueCharactersPasswordValidatorCfg configuration)
+  {
+    configuration.addUniqueCharactersChangeListener(this);
+    currentConfig = configuration;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public void finalizePasswordValidator()
+  {
+    currentConfig.removeUniqueCharactersChangeListener(this);
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override()
+  public boolean passwordIsAcceptable(ByteString newPassword,
+                                      Set<ByteString> currentPasswords,
+                                      Operation operation, Entry userEntry,
+                                      StringBuilder invalidReason)
+  {
+    // Get a handle to the current configuration and see if we need to count
+    // the number of unique characters in the password.
+    UniqueCharactersPasswordValidatorCfg config = currentConfig;
+    int minUniqueCharacters = config.getMinimumUniqueCharacters();
+    if (minUniqueCharacters <= 0)
+    {
+      // We don't need to check anything, so the password will be acceptable.
+      return true;
+    }
+
+
+
+    // Create a set that will be used to keep track of the unique characters
+    // contained in the proposed password.
+    HashSet<Character> passwordCharacters = new HashSet<Character>();
+
+    // Iterate through the characters in the new password and place them in the
+    // set as needed.  If we should behave in a case-insensitive manner, then
+    // convert all the characters to lowercase first.
+    String passwordString = newPassword.stringValue();
+    if (! config.isCaseSensitiveValidation())
+    {
+      passwordString = passwordString.toLowerCase();
+    }
+
+    for (int i=0; i < passwordString.length(); i++)
+    {
+      passwordCharacters.add(passwordString.charAt(i));
+    }
+
+    // If the size of the password characters set is less than the minimum
+    // number of allowed unique characters, then we will reject the password.
+    if (passwordCharacters.size() < minUniqueCharacters)
+    {
+      int    msgID   = MSGID_UNIQUECHARS_VALIDATOR_NOT_ENOUGH_UNIQUE_CHARS;
+      String message = getMessage(msgID, minUniqueCharacters);
+      invalidReason.append(message);
+      return false;
+    }
+
+    return true;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  public boolean isConfigurationChangeAcceptable(
+                      UniqueCharactersPasswordValidatorCfg configuration,
+                      List<String> unacceptableReasons)
+  {
+    // All of the necessary validation should have been performed automatically,
+    // so if we get to this point then the new configuration will be acceptable.
+    return true;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   */
+  public ConfigChangeResult applyConfigurationChange(
+                      UniqueCharactersPasswordValidatorCfg configuration)
+  {
+    ResultCode        resultCode          = ResultCode.SUCCESS;
+    boolean           adminActionRequired = false;
+    ArrayList<String> messages            = new ArrayList<String>();
+
+    // For this password validator, we will always be able to successfully apply
+    // the new configuration.
+    currentConfig = configuration;
+
+    return new ConfigChangeResult(resultCode, adminActionRequired, messages);
+  }
+}
+
diff --git a/opendj-sdk/opends/src/server/org/opends/server/messages/ExtensionsMessages.java b/opendj-sdk/opends/src/server/org/opends/server/messages/ExtensionsMessages.java
index ae4670c..bc856f8 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/messages/ExtensionsMessages.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/messages/ExtensionsMessages.java
@@ -4807,6 +4807,25 @@
 
 
   /**
+   * The message ID for the message that will be used if the same character
+   * appears too many times in consecutive order in a given password.  This
+   * takes a single argument, which is the maximum number of times a character
+   * may appear in consecutive order.
+   */
+  public static final int MSGID_REPEATEDCHARS_VALIDATOR_TOO_MANY_CONSECUTIVE =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 457;
+
+
+  /**
+   * The message ID for the message that will be used if a given password does
+   * not have enough unique characters.  This takes a single argument, which is
+   * the minimum number of unique characters that a password may contain.
+   */
+  public static final int MSGID_UNIQUECHARS_VALIDATOR_NOT_ENOUGH_UNIQUE_CHARS =
+       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 458;
+
+
+  /**
    * Associates a set of generic messages with the message IDs defined in this
    * class.
    */
@@ -6930,6 +6949,19 @@
     registerMessage(MSGID_DYNAMICGROUP_CANNOT_RETURN_ENTRY,
                     "The server encountered a timeout while attempting to " +
                     "add user %s to the member list for dynamic group %s.");
+
+
+    registerMessage(MSGID_REPEATEDCHARS_VALIDATOR_TOO_MANY_CONSECUTIVE,
+                    "The provided password contained too many instances " +
+                    "of the same character appearing consecutively.  The " +
+                    "maximum number of times the same character may appear " +
+                    "consecutively in a password is %d.");
+
+
+    registerMessage(MSGID_UNIQUECHARS_VALIDATOR_NOT_ENOUGH_UNIQUE_CHARS,
+                    "The provided password does not contain enough unique " +
+                    "characters.  The minimum number of unique characters " +
+                    "that may appear in a user password is %d.");
   }
 }
 
diff --git a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/RepeatedCharactersPasswordValidatorTestCase.java b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/RepeatedCharactersPasswordValidatorTestCase.java
new file mode 100644
index 0000000..c0672b7
--- /dev/null
+++ b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/RepeatedCharactersPasswordValidatorTestCase.java
@@ -0,0 +1,699 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License").  You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE
+ * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
+ * add the following below this CDDL HEADER, with the fields enclosed
+ * by brackets "[]" replaced with your own identifying information:
+ *      Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ *
+ *      Portions Copyright 2006-2007 Sun Microsystems, Inc.
+ */
+package org.opends.server.extensions;
+
+
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import org.opends.server.TestCaseUtils;
+import org.opends.server.admin.std.meta.
+            RepeatedCharactersPasswordValidatorCfgDefn;
+import org.opends.server.admin.std.server.
+            RepeatedCharactersPasswordValidatorCfg;
+import org.opends.server.admin.server.AdminTestCaseUtils;
+import org.opends.server.config.ConfigEntry;
+import org.opends.server.config.ConfigException;
+import org.opends.server.core.DirectoryServer;
+import org.opends.server.core.ModifyOperation;
+import org.opends.server.protocols.asn1.ASN1OctetString;
+import org.opends.server.protocols.internal.InternalClientConnection;
+import org.opends.server.types.Attribute;
+import org.opends.server.types.ByteString;
+import org.opends.server.types.ConfigChangeResult;
+import org.opends.server.types.Control;
+import org.opends.server.types.DN;
+import org.opends.server.types.Entry;
+import org.opends.server.types.InitializationException;
+import org.opends.server.types.Modification;
+import org.opends.server.types.ModificationType;
+import org.opends.server.types.ResultCode;
+
+import static org.testng.Assert.*;
+
+
+
+/**
+ * A set of test cases for the repeated characters password validator.
+ */
+public class RepeatedCharactersPasswordValidatorTestCase
+       extends ExtensionsTestCase
+{
+  /**
+   * Ensures that the Directory Server is running.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @BeforeClass()
+  public void startServer()
+         throws Exception
+  {
+    TestCaseUtils.startServer();
+  }
+
+
+
+  /**
+   * Retrieves a set of valid configuration entries that may be used to
+   * initialize the validator.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @DataProvider(name = "validConfigs")
+  public Object[][] getValidConfigs()
+         throws Exception
+  {
+    List<Entry> entries = TestCaseUtils.makeEntries(
+         "dn: cn=Repeated Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-repeated-characters-password-validator",
+         "cn: Repeated Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "RepeatedCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-maximum-consecutive-length: 2",
+         "ds-cfg-case-sensitive-validation: false",
+         "",
+         "dn: cn=Repeated Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-repeated-characters-password-validator",
+         "cn: Repeated Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "RepeatedCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-maximum-consecutive-length: 2",
+         "ds-cfg-case-sensitive-validation: true",
+         "",
+         "dn: cn=Repeated Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-repeated-characters-password-validator",
+         "cn: Repeated Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "RepeatedCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-maximum-consecutive-length: 0",
+         "ds-cfg-case-sensitive-validation: false");
+
+    Object[][] array = new Object[entries.size()][1];
+    for (int i=0; i < array.length; i++)
+    {
+      array[i] = new Object[] { entries.get(i) };
+    }
+
+    return array;
+  }
+
+
+
+  /**
+   * Tests the process of initializing the server with valid configurations.
+   *
+   * @param  entry  The configuration entry to use for the initialization.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test(dataProvider = "validConfigs")
+  public void testInitializeWithValidConfigs(Entry e)
+         throws Exception
+  {
+    RepeatedCharactersPasswordValidatorCfg configuration =
+         AdminTestCaseUtils.getConfiguration(
+              RepeatedCharactersPasswordValidatorCfgDefn.getInstance(), e);
+
+    RepeatedCharactersPasswordValidator validator =
+         new RepeatedCharactersPasswordValidator();
+    validator.initializePasswordValidator(configuration);
+    validator.finalizePasswordValidator();
+  }
+
+
+
+  /**
+   * Retrieves a set of invalid configuration entries.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @DataProvider(name = "invalidConfigs")
+  public Object[][] getInvalidConfigs()
+         throws Exception
+  {
+    List<Entry> entries = TestCaseUtils.makeEntries(
+         // Missing maximum consecutive length
+         "dn: cn=Repeated Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-repeated-characters-password-validator",
+         "cn: Repeated Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "RepeatedCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-case-sensitive-validation: false",
+         "",
+         // Missing case-sensitive validation
+         "dn: cn=Repeated Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-repeated-characters-password-validator",
+         "cn: Repeated Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "RepeatedCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-maximum-consecutive-length: 2",
+         "",
+         // Non-numeric maximum consecutive length
+         "dn: cn=Repeated Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-repeated-characters-password-validator",
+         "cn: Repeated Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "RepeatedCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-maximum-consecutive-length: non-numeric",
+         "ds-cfg-case-sensitive-validation: false",
+         "",
+         // Non-boolean case-sensitive validation
+         "dn: cn=Repeated Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-repeated-characters-password-validator",
+         "cn: Repeated Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "RepeatedCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-maximum-consecutive-length: 2",
+         "ds-cfg-case-sensitive-validation: non-boolean",
+         "",
+         // Maximum consecutive length out of range.
+         "dn: cn=Repeated Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-repeated-characters-password-validator",
+         "cn: Repeated Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "RepeatedCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-maximum-consecutive-length: -1",
+         "ds-cfg-case-sensitive-validation: false");
+
+    Object[][] array = new Object[entries.size()][1];
+    for (int i=0; i < array.length; i++)
+    {
+      array[i] = new Object[] { entries.get(i) };
+    }
+
+    return array;
+  }
+
+
+
+  /**
+   * Tests the process of initializing the server with invalid configurations.
+   *
+   * @param  entry  The configuration entry to use for the initialization.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test(dataProvider = "invalidConfigs",
+        expectedExceptions = { ConfigException.class,
+                               InitializationException.class })
+  public void testInitializeWithInvalidConfigs(Entry e)
+         throws Exception
+  {
+    RepeatedCharactersPasswordValidatorCfg configuration =
+         AdminTestCaseUtils.getConfiguration(
+              RepeatedCharactersPasswordValidatorCfgDefn.getInstance(), e);
+
+    RepeatedCharactersPasswordValidator validator =
+         new RepeatedCharactersPasswordValidator();
+    validator.initializePasswordValidator(configuration);
+  }
+
+
+
+  /**
+   * Tests the {@code passwordIsAcceptable} method with a password that falls
+   * within the constraints of the password validator.  Case-sensitivity will
+   * not be an issue.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testPasswordIsAcceptable2Consecutive()
+         throws Exception
+  {
+    TestCaseUtils.initializeTestBackend(true);
+    Entry userEntry = TestCaseUtils.makeEntry(
+         "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: doesntmatter");
+
+    Entry validatorEntry = TestCaseUtils.makeEntry(
+         "dn: cn=Repeated Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-repeated-characters-password-validator",
+         "cn: Repeated Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "RepeatedCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-maximum-consecutive-length: 2",
+         "ds-cfg-case-sensitive-validation: false");
+
+    RepeatedCharactersPasswordValidatorCfg configuration =
+         AdminTestCaseUtils.getConfiguration(
+              RepeatedCharactersPasswordValidatorCfgDefn.getInstance(),
+              validatorEntry);
+
+    RepeatedCharactersPasswordValidator validator =
+         new RepeatedCharactersPasswordValidator();
+    validator.initializePasswordValidator(configuration);
+
+    ASN1OctetString password = new ASN1OctetString("password");
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.REPLACE,
+                              new Attribute("userpassword", "password")));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         new ModifyOperation(conn, conn.nextOperationID(), conn.nextMessageID(),
+                             new ArrayList<Control>(),
+                             DN.decode("uid=test.user,o=test"), mods);
+
+    StringBuilder invalidReason = new StringBuilder();
+    assertTrue(validator.passwordIsAcceptable(password,
+                              new HashSet<ByteString>(0), modifyOperation,
+                              userEntry, invalidReason),
+               invalidReason.toString());
+
+    validator.finalizePasswordValidator();
+  }
+
+
+
+  /**
+   * Tests the {@code passwordIsAcceptable} method with a password that falls
+   * outside of the constraints of the password validator.  Case-sensitivity
+   * will not be an issue.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testPasswordIsAcceptable3Consecutive()
+         throws Exception
+  {
+    TestCaseUtils.initializeTestBackend(true);
+    Entry userEntry = TestCaseUtils.makeEntry(
+         "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: doesntmatter");
+
+    Entry validatorEntry = TestCaseUtils.makeEntry(
+         "dn: cn=Repeated Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-repeated-characters-password-validator",
+         "cn: Repeated Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "RepeatedCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-maximum-consecutive-length: 2",
+         "ds-cfg-case-sensitive-validation: false");
+
+    RepeatedCharactersPasswordValidatorCfg configuration =
+         AdminTestCaseUtils.getConfiguration(
+              RepeatedCharactersPasswordValidatorCfgDefn.getInstance(),
+              validatorEntry);
+
+    RepeatedCharactersPasswordValidator validator =
+         new RepeatedCharactersPasswordValidator();
+    validator.initializePasswordValidator(configuration);
+
+    ASN1OctetString password = new ASN1OctetString("passsword");
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.REPLACE,
+                              new Attribute("userpassword", "passsword")));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         new ModifyOperation(conn, conn.nextOperationID(), conn.nextMessageID(),
+                             new ArrayList<Control>(),
+                             DN.decode("uid=test.user,o=test"), mods);
+
+    StringBuilder invalidReason = new StringBuilder();
+    assertFalse(validator.passwordIsAcceptable(password,
+                               new HashSet<ByteString>(0), modifyOperation,
+                               userEntry, invalidReason));
+
+    validator.finalizePasswordValidator();
+  }
+
+
+
+  /**
+   * Tests the {@code passwordIsAcceptable} method with a password that falls
+   * within the constraints of the password validator only because it is
+   * configured to operate in a case-sensitive manner.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testPasswordIsAcceptableCaseSensitive()
+         throws Exception
+  {
+    TestCaseUtils.initializeTestBackend(true);
+    Entry userEntry = TestCaseUtils.makeEntry(
+         "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: doesntmatter");
+
+    Entry validatorEntry = TestCaseUtils.makeEntry(
+         "dn: cn=Repeated Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-repeated-characters-password-validator",
+         "cn: Repeated Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "RepeatedCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-maximum-consecutive-length: 2",
+         "ds-cfg-case-sensitive-validation: true");
+
+    RepeatedCharactersPasswordValidatorCfg configuration =
+         AdminTestCaseUtils.getConfiguration(
+              RepeatedCharactersPasswordValidatorCfgDefn.getInstance(),
+              validatorEntry);
+
+    RepeatedCharactersPasswordValidator validator =
+         new RepeatedCharactersPasswordValidator();
+    validator.initializePasswordValidator(configuration);
+
+    ASN1OctetString password = new ASN1OctetString("passSword");
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.REPLACE,
+                              new Attribute("userpassword", "passSword")));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         new ModifyOperation(conn, conn.nextOperationID(), conn.nextMessageID(),
+                             new ArrayList<Control>(),
+                             DN.decode("uid=test.user,o=test"), mods);
+
+    StringBuilder invalidReason = new StringBuilder();
+    assertTrue(validator.passwordIsAcceptable(password,
+                              new HashSet<ByteString>(0), modifyOperation,
+                              userEntry, invalidReason),
+               invalidReason.toString());
+
+    validator.finalizePasswordValidator();
+  }
+
+
+
+  /**
+   * Tests the {@code passwordIsAcceptable} method with a password that falls
+   * outside of the constraints of the password validator because it is
+   * configured to operate in a case-insensitive manner.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testPasswordIsAcceptableCaseInsensitive()
+         throws Exception
+  {
+    TestCaseUtils.initializeTestBackend(true);
+    Entry userEntry = TestCaseUtils.makeEntry(
+         "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: doesntmatter");
+
+    Entry validatorEntry = TestCaseUtils.makeEntry(
+         "dn: cn=Repeated Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-repeated-characters-password-validator",
+         "cn: Repeated Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "RepeatedCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-maximum-consecutive-length: 2",
+         "ds-cfg-case-sensitive-validation: false");
+
+    RepeatedCharactersPasswordValidatorCfg configuration =
+         AdminTestCaseUtils.getConfiguration(
+              RepeatedCharactersPasswordValidatorCfgDefn.getInstance(),
+              validatorEntry);
+
+    RepeatedCharactersPasswordValidator validator =
+         new RepeatedCharactersPasswordValidator();
+    validator.initializePasswordValidator(configuration);
+
+    ASN1OctetString password = new ASN1OctetString("passSword");
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.REPLACE,
+                              new Attribute("userpassword", "passSword")));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         new ModifyOperation(conn, conn.nextOperationID(), conn.nextMessageID(),
+                             new ArrayList<Control>(),
+                             DN.decode("uid=test.user,o=test"), mods);
+
+    StringBuilder invalidReason = new StringBuilder();
+    assertFalse(validator.passwordIsAcceptable(password,
+                               new HashSet<ByteString>(0), modifyOperation,
+                               userEntry, invalidReason));
+
+    validator.finalizePasswordValidator();
+  }
+
+
+
+  /**
+   * Tests the {@code passwordIsAcceptable} method when the validator is
+   * configured to accept any number of repeated characters.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testPasswordIsAcceptableUnlimitedRepeats()
+         throws Exception
+  {
+    TestCaseUtils.initializeTestBackend(true);
+    Entry userEntry = TestCaseUtils.makeEntry(
+         "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: doesntmatter");
+
+    Entry validatorEntry = TestCaseUtils.makeEntry(
+         "dn: cn=Repeated Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-repeated-characters-password-validator",
+         "cn: Repeated Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "RepeatedCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-maximum-consecutive-length: 0",
+         "ds-cfg-case-sensitive-validation: true");
+
+    RepeatedCharactersPasswordValidatorCfg configuration =
+         AdminTestCaseUtils.getConfiguration(
+              RepeatedCharactersPasswordValidatorCfgDefn.getInstance(),
+              validatorEntry);
+
+    RepeatedCharactersPasswordValidator validator =
+         new RepeatedCharactersPasswordValidator();
+    validator.initializePasswordValidator(configuration);
+
+    ASN1OctetString password = new ASN1OctetString("aaaaaaaa");
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.REPLACE,
+                              new Attribute("userpassword", "aaaaaaaa")));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         new ModifyOperation(conn, conn.nextOperationID(), conn.nextMessageID(),
+                             new ArrayList<Control>(),
+                             DN.decode("uid=test.user,o=test"), mods);
+
+    StringBuilder invalidReason = new StringBuilder();
+    assertTrue(validator.passwordIsAcceptable(password,
+                              new HashSet<ByteString>(0), modifyOperation,
+                              userEntry, invalidReason),
+               invalidReason.toString());
+
+    validator.finalizePasswordValidator();
+  }
+
+
+
+  /**
+   * Tests the ability of the password validator to change its behavior when
+   * the configuration is updated.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testPasswordIsAcceptableConfigurationChange()
+         throws Exception
+  {
+    TestCaseUtils.initializeTestBackend(true);
+    Entry userEntry = TestCaseUtils.makeEntry(
+         "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: doesntmatter");
+
+    Entry validatorEntry = TestCaseUtils.makeEntry(
+         "dn: cn=Repeated Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-repeated-characters-password-validator",
+         "cn: Repeated Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "RepeatedCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-maximum-consecutive-length: 0",
+         "ds-cfg-case-sensitive-validation: true");
+
+    RepeatedCharactersPasswordValidatorCfg configuration =
+         AdminTestCaseUtils.getConfiguration(
+              RepeatedCharactersPasswordValidatorCfgDefn.getInstance(),
+              validatorEntry);
+
+    RepeatedCharactersPasswordValidator validator =
+         new RepeatedCharactersPasswordValidator();
+    validator.initializePasswordValidator(configuration);
+
+    ASN1OctetString password = new ASN1OctetString("aaaaaaaa");
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.REPLACE,
+                              new Attribute("userpassword", "aaaaaaaa")));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         new ModifyOperation(conn, conn.nextOperationID(), conn.nextMessageID(),
+                             new ArrayList<Control>(),
+                             DN.decode("uid=test.user,o=test"), mods);
+
+    StringBuilder invalidReason = new StringBuilder();
+    assertTrue(validator.passwordIsAcceptable(password,
+                              new HashSet<ByteString>(0), modifyOperation,
+                              userEntry, invalidReason),
+               invalidReason.toString());
+
+    Entry updatedValidatorEntry = TestCaseUtils.makeEntry(
+         "dn: cn=Repeated Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-repeated-characters-password-validator",
+         "cn: Repeated Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "RepeatedCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-maximum-consecutive-length: 2",
+         "ds-cfg-case-sensitive-validation: true");
+
+    RepeatedCharactersPasswordValidatorCfg updatedConfiguration =
+         AdminTestCaseUtils.getConfiguration(
+              RepeatedCharactersPasswordValidatorCfgDefn.getInstance(),
+              updatedValidatorEntry);
+
+    ArrayList<String> unacceptableReasons = new ArrayList<String>();
+    assertTrue(validator.isConfigurationChangeAcceptable(updatedConfiguration,
+                                                         unacceptableReasons),
+               String.valueOf(unacceptableReasons));
+
+    ConfigChangeResult changeResult =
+         validator.applyConfigurationChange(updatedConfiguration);
+    assertEquals(changeResult.getResultCode(), ResultCode.SUCCESS);
+
+    assertFalse(validator.passwordIsAcceptable(password,
+                               new HashSet<ByteString>(0), modifyOperation,
+                               userEntry, invalidReason));
+
+    validator.finalizePasswordValidator();
+  }
+}
+
diff --git a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/UniqueCharactersPasswordValidatorTestCase.java b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/UniqueCharactersPasswordValidatorTestCase.java
new file mode 100644
index 0000000..d5e4fe0
--- /dev/null
+++ b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/UniqueCharactersPasswordValidatorTestCase.java
@@ -0,0 +1,699 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License").  You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE
+ * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at
+ * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
+ * add the following below this CDDL HEADER, with the fields enclosed
+ * by brackets "[]" replaced with your own identifying information:
+ *      Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ *
+ *      Portions Copyright 2006-2007 Sun Microsystems, Inc.
+ */
+package org.opends.server.extensions;
+
+
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import org.opends.server.TestCaseUtils;
+import org.opends.server.admin.std.meta.
+            UniqueCharactersPasswordValidatorCfgDefn;
+import org.opends.server.admin.std.server.
+            UniqueCharactersPasswordValidatorCfg;
+import org.opends.server.admin.server.AdminTestCaseUtils;
+import org.opends.server.config.ConfigEntry;
+import org.opends.server.config.ConfigException;
+import org.opends.server.core.DirectoryServer;
+import org.opends.server.core.ModifyOperation;
+import org.opends.server.protocols.asn1.ASN1OctetString;
+import org.opends.server.protocols.internal.InternalClientConnection;
+import org.opends.server.types.Attribute;
+import org.opends.server.types.ByteString;
+import org.opends.server.types.ConfigChangeResult;
+import org.opends.server.types.Control;
+import org.opends.server.types.DN;
+import org.opends.server.types.Entry;
+import org.opends.server.types.InitializationException;
+import org.opends.server.types.Modification;
+import org.opends.server.types.ModificationType;
+import org.opends.server.types.ResultCode;
+
+import static org.testng.Assert.*;
+
+
+
+/**
+ * A set of test cases for the unique characters password validator.
+ */
+public class UniqueCharactersPasswordValidatorTestCase
+       extends ExtensionsTestCase
+{
+  /**
+   * Ensures that the Directory Server is running.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @BeforeClass()
+  public void startServer()
+         throws Exception
+  {
+    TestCaseUtils.startServer();
+  }
+
+
+
+  /**
+   * Retrieves a set of valid configuration entries that may be used to
+   * initialize the validator.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @DataProvider(name = "validConfigs")
+  public Object[][] getValidConfigs()
+         throws Exception
+  {
+    List<Entry> entries = TestCaseUtils.makeEntries(
+         "dn: cn=Unique Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-unique-characters-password-validator",
+         "cn: Unique Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "UniqueCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-minimum-unique-characters: 5",
+         "ds-cfg-case-sensitive-validation: false",
+         "",
+         "dn: cn=Unique Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-unique-characters-password-validator",
+         "cn: Unique Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "UniqueCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-minimum-unique-characters: 5",
+         "ds-cfg-case-sensitive-validation: true",
+         "",
+         "dn: cn=Unique Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-unique-characters-password-validator",
+         "cn: Unique Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "UniqueCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-minimum-unique-characters: 0",
+         "ds-cfg-case-sensitive-validation: false");
+
+    Object[][] array = new Object[entries.size()][1];
+    for (int i=0; i < array.length; i++)
+    {
+      array[i] = new Object[] { entries.get(i) };
+    }
+
+    return array;
+  }
+
+
+
+  /**
+   * Tests the process of initializing the server with valid configurations.
+   *
+   * @param  entry  The configuration entry to use for the initialization.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test(dataProvider = "validConfigs")
+  public void testInitializeWithValidConfigs(Entry e)
+         throws Exception
+  {
+    UniqueCharactersPasswordValidatorCfg configuration =
+         AdminTestCaseUtils.getConfiguration(
+              UniqueCharactersPasswordValidatorCfgDefn.getInstance(), e);
+
+    UniqueCharactersPasswordValidator validator =
+         new UniqueCharactersPasswordValidator();
+    validator.initializePasswordValidator(configuration);
+    validator.finalizePasswordValidator();
+  }
+
+
+
+  /**
+   * Retrieves a set of invalid configuration entries.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @DataProvider(name = "invalidConfigs")
+  public Object[][] getInvalidConfigs()
+         throws Exception
+  {
+    List<Entry> entries = TestCaseUtils.makeEntries(
+         // Missing minimum unique characters
+         "dn: cn=Unique Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-unique-characters-password-validator",
+         "cn: Unique Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "UniqueCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-case-sensitive-validation: false",
+         "",
+         // Missing case-sensitive validation
+         "dn: cn=Unique Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-unique-characters-password-validator",
+         "cn: Unique Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "UniqueCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-minimum-unique-characters: 5",
+         "",
+         // Non-numeric minimum unique characters
+         "dn: cn=Unique Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-unique-characters-password-validator",
+         "cn: Unique Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "UniqueCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-minimum-unique-characters: non-numeric",
+         "ds-cfg-case-sensitive-validation: false",
+         "",
+         // Non-boolean case-sensitive validation
+         "dn: cn=Unique Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-unique-characters-password-validator",
+         "cn: Unique Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "UniqueCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-minimum-unique-characters: 5",
+         "ds-cfg-case-sensitive-validation: non-boolean",
+         "",
+         // Minimum unique characters out of range.
+         "dn: cn=Unique Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-unique-characters-password-validator",
+         "cn: Unique Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "UniqueCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-minimum-unique-characters: -1",
+         "ds-cfg-case-sensitive-validation: false");
+
+    Object[][] array = new Object[entries.size()][1];
+    for (int i=0; i < array.length; i++)
+    {
+      array[i] = new Object[] { entries.get(i) };
+    }
+
+    return array;
+  }
+
+
+
+  /**
+   * Tests the process of initializing the server with invalid configurations.
+   *
+   * @param  entry  The configuration entry to use for the initialization.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test(dataProvider = "invalidConfigs",
+        expectedExceptions = { ConfigException.class,
+                               InitializationException.class })
+  public void testInitializeWithInvalidConfigs(Entry e)
+         throws Exception
+  {
+    UniqueCharactersPasswordValidatorCfg configuration =
+         AdminTestCaseUtils.getConfiguration(
+              UniqueCharactersPasswordValidatorCfgDefn.getInstance(), e);
+
+    UniqueCharactersPasswordValidator validator =
+         new UniqueCharactersPasswordValidator();
+    validator.initializePasswordValidator(configuration);
+  }
+
+
+
+  /**
+   * Tests the {@code passwordIsAcceptable} method with a password that falls
+   * within the constraints of the password validator.  Case-sensitivity will
+   * not be an issue.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testPasswordIsAcceptable7Unique()
+         throws Exception
+  {
+    TestCaseUtils.initializeTestBackend(true);
+    Entry userEntry = TestCaseUtils.makeEntry(
+         "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: doesntmatter");
+
+    Entry validatorEntry = TestCaseUtils.makeEntry(
+         "dn: cn=Unique Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-unique-characters-password-validator",
+         "cn: Unique Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "UniqueCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-minimum-unique-characters: 5",
+         "ds-cfg-case-sensitive-validation: false");
+
+    UniqueCharactersPasswordValidatorCfg configuration =
+         AdminTestCaseUtils.getConfiguration(
+              UniqueCharactersPasswordValidatorCfgDefn.getInstance(),
+              validatorEntry);
+
+    UniqueCharactersPasswordValidator validator =
+         new UniqueCharactersPasswordValidator();
+    validator.initializePasswordValidator(configuration);
+
+    ASN1OctetString password = new ASN1OctetString("password");
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.REPLACE,
+                              new Attribute("userpassword", "password")));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         new ModifyOperation(conn, conn.nextOperationID(), conn.nextMessageID(),
+                             new ArrayList<Control>(),
+                             DN.decode("uid=test.user,o=test"), mods);
+
+    StringBuilder invalidReason = new StringBuilder();
+    assertTrue(validator.passwordIsAcceptable(password,
+                              new HashSet<ByteString>(0), modifyOperation,
+                              userEntry, invalidReason),
+               invalidReason.toString());
+
+    validator.finalizePasswordValidator();
+  }
+
+
+
+  /**
+   * Tests the {@code passwordIsAcceptable} method with a password that falls
+   * outside the constraints of the password validator.  Case-sensitivity will
+   * not be an issue.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testPasswordIsAcceptable4Unique()
+         throws Exception
+  {
+    TestCaseUtils.initializeTestBackend(true);
+    Entry userEntry = TestCaseUtils.makeEntry(
+         "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: doesntmatter");
+
+    Entry validatorEntry = TestCaseUtils.makeEntry(
+         "dn: cn=Unique Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-unique-characters-password-validator",
+         "cn: Unique Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "UniqueCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-minimum-unique-characters: 5",
+         "ds-cfg-case-sensitive-validation: false");
+
+    UniqueCharactersPasswordValidatorCfg configuration =
+         AdminTestCaseUtils.getConfiguration(
+              UniqueCharactersPasswordValidatorCfgDefn.getInstance(),
+              validatorEntry);
+
+    UniqueCharactersPasswordValidator validator =
+         new UniqueCharactersPasswordValidator();
+    validator.initializePasswordValidator(configuration);
+
+    ASN1OctetString password = new ASN1OctetString("passw");
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.REPLACE,
+                              new Attribute("userpassword", "passw")));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         new ModifyOperation(conn, conn.nextOperationID(), conn.nextMessageID(),
+                             new ArrayList<Control>(),
+                             DN.decode("uid=test.user,o=test"), mods);
+
+    StringBuilder invalidReason = new StringBuilder();
+    assertFalse(validator.passwordIsAcceptable(password,
+                               new HashSet<ByteString>(0), modifyOperation,
+                               userEntry, invalidReason));
+
+    validator.finalizePasswordValidator();
+  }
+
+
+
+  /**
+   * Tests the {@code passwordIsAcceptable} method with a password that falls
+   * within the constraints of the password validator only because it uses
+   * case-sensitive validation.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testPasswordIsAcceptableCaseSensitive()
+         throws Exception
+  {
+    TestCaseUtils.initializeTestBackend(true);
+    Entry userEntry = TestCaseUtils.makeEntry(
+         "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: doesntmatter");
+
+    Entry validatorEntry = TestCaseUtils.makeEntry(
+         "dn: cn=Unique Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-unique-characters-password-validator",
+         "cn: Unique Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "UniqueCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-minimum-unique-characters: 5",
+         "ds-cfg-case-sensitive-validation: true");
+
+    UniqueCharactersPasswordValidatorCfg configuration =
+         AdminTestCaseUtils.getConfiguration(
+              UniqueCharactersPasswordValidatorCfgDefn.getInstance(),
+              validatorEntry);
+
+    UniqueCharactersPasswordValidator validator =
+         new UniqueCharactersPasswordValidator();
+    validator.initializePasswordValidator(configuration);
+
+    ASN1OctetString password = new ASN1OctetString("pasSw");
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.REPLACE,
+                              new Attribute("userpassword", "pasSw")));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         new ModifyOperation(conn, conn.nextOperationID(), conn.nextMessageID(),
+                             new ArrayList<Control>(),
+                             DN.decode("uid=test.user,o=test"), mods);
+
+    StringBuilder invalidReason = new StringBuilder();
+    assertTrue(validator.passwordIsAcceptable(password,
+                              new HashSet<ByteString>(0), modifyOperation,
+                              userEntry, invalidReason),
+               invalidReason.toString());
+
+    validator.finalizePasswordValidator();
+  }
+
+
+
+  /**
+   * Tests the {@code passwordIsAcceptable} method with a password that falls
+   * outside the constraints of the password validator because it uses
+   * case-insensitive validation.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testPasswordIsAcceptableCaseInsensitive()
+         throws Exception
+  {
+    TestCaseUtils.initializeTestBackend(true);
+    Entry userEntry = TestCaseUtils.makeEntry(
+         "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: doesntmatter");
+
+    Entry validatorEntry = TestCaseUtils.makeEntry(
+         "dn: cn=Unique Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-unique-characters-password-validator",
+         "cn: Unique Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "UniqueCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-minimum-unique-characters: 5",
+         "ds-cfg-case-sensitive-validation: false");
+
+    UniqueCharactersPasswordValidatorCfg configuration =
+         AdminTestCaseUtils.getConfiguration(
+              UniqueCharactersPasswordValidatorCfgDefn.getInstance(),
+              validatorEntry);
+
+    UniqueCharactersPasswordValidator validator =
+         new UniqueCharactersPasswordValidator();
+    validator.initializePasswordValidator(configuration);
+
+    ASN1OctetString password = new ASN1OctetString("pasSw");
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.REPLACE,
+                              new Attribute("userpassword", "pasSw")));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         new ModifyOperation(conn, conn.nextOperationID(), conn.nextMessageID(),
+                             new ArrayList<Control>(),
+                             DN.decode("uid=test.user,o=test"), mods);
+
+    StringBuilder invalidReason = new StringBuilder();
+    assertFalse(validator.passwordIsAcceptable(password,
+                               new HashSet<ByteString>(0), modifyOperation,
+                               userEntry, invalidReason));
+
+    validator.finalizePasswordValidator();
+  }
+
+
+
+  /**
+   * Tests the {@code passwordIsAcceptable} method when the validator is
+   * configured to accept any number of unique characters.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testPasswordIsAcceptableAnyNumberOfCharacters()
+         throws Exception
+  {
+    TestCaseUtils.initializeTestBackend(true);
+    Entry userEntry = TestCaseUtils.makeEntry(
+         "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: doesntmatter");
+
+    Entry validatorEntry = TestCaseUtils.makeEntry(
+         "dn: cn=Unique Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-unique-characters-password-validator",
+         "cn: Unique Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "UniqueCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-minimum-unique-characters: 0",
+         "ds-cfg-case-sensitive-validation: true");
+
+    UniqueCharactersPasswordValidatorCfg configuration =
+         AdminTestCaseUtils.getConfiguration(
+              UniqueCharactersPasswordValidatorCfgDefn.getInstance(),
+              validatorEntry);
+
+    UniqueCharactersPasswordValidator validator =
+         new UniqueCharactersPasswordValidator();
+    validator.initializePasswordValidator(configuration);
+
+    ASN1OctetString password = new ASN1OctetString("aaaaaaaa");
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.REPLACE,
+                              new Attribute("userpassword", "aaaaaaaa")));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         new ModifyOperation(conn, conn.nextOperationID(), conn.nextMessageID(),
+                             new ArrayList<Control>(),
+                             DN.decode("uid=test.user,o=test"), mods);
+
+    StringBuilder invalidReason = new StringBuilder();
+    assertTrue(validator.passwordIsAcceptable(password,
+                              new HashSet<ByteString>(0), modifyOperation,
+                              userEntry, invalidReason),
+               invalidReason.toString());
+
+    validator.finalizePasswordValidator();
+  }
+
+
+
+  /**
+   * Tests the ability of the password validator to change its behavior when
+   * the configuration is updated.
+   *
+   * @throws  Exception  If an unexpected problem occurs.
+   */
+  @Test()
+  public void testPasswordIsAcceptableConfigurationChange()
+         throws Exception
+  {
+    TestCaseUtils.initializeTestBackend(true);
+    Entry userEntry = TestCaseUtils.makeEntry(
+         "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: doesntmatter");
+
+    Entry validatorEntry = TestCaseUtils.makeEntry(
+         "dn: cn=Unique Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-unique-characters-password-validator",
+         "cn: Unique Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "UniqueCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-minimum-unique-characters: 0",
+         "ds-cfg-case-sensitive-validation: true");
+
+    UniqueCharactersPasswordValidatorCfg configuration =
+         AdminTestCaseUtils.getConfiguration(
+              UniqueCharactersPasswordValidatorCfgDefn.getInstance(),
+              validatorEntry);
+
+    UniqueCharactersPasswordValidator validator =
+         new UniqueCharactersPasswordValidator();
+    validator.initializePasswordValidator(configuration);
+
+    ASN1OctetString password = new ASN1OctetString("aaaaaaaa");
+    ArrayList<Modification> mods = new ArrayList<Modification>();
+    mods.add(new Modification(ModificationType.REPLACE,
+                              new Attribute("userpassword", "aaaaaaaa")));
+
+    InternalClientConnection conn =
+         InternalClientConnection.getRootConnection();
+    ModifyOperation modifyOperation =
+         new ModifyOperation(conn, conn.nextOperationID(), conn.nextMessageID(),
+                             new ArrayList<Control>(),
+                             DN.decode("uid=test.user,o=test"), mods);
+
+    StringBuilder invalidReason = new StringBuilder();
+    assertTrue(validator.passwordIsAcceptable(password,
+                              new HashSet<ByteString>(0), modifyOperation,
+                              userEntry, invalidReason),
+               invalidReason.toString());
+
+    Entry updatedValidatorEntry = TestCaseUtils.makeEntry(
+         "dn: cn=Unique Characters,cn=Password Validators,cn=config",
+         "objectClass: top",
+         "objectClass: ds-cfg-password-validator",
+         "objectClass: ds-cfg-unique-characters-password-validator",
+         "cn: Unique Characters",
+         "ds-cfg-password-validator-class: org.opends.server.extensions." +
+              "UniqueCharactersPasswordValidator",
+         "ds-cfg-password-validator-enabled: true",
+         "ds-cfg-minimum-unique-characters: 5",
+         "ds-cfg-case-sensitive-validation: true");
+
+    UniqueCharactersPasswordValidatorCfg updatedConfiguration =
+         AdminTestCaseUtils.getConfiguration(
+              UniqueCharactersPasswordValidatorCfgDefn.getInstance(),
+              updatedValidatorEntry);
+
+    ArrayList<String> unacceptableReasons = new ArrayList<String>();
+    assertTrue(validator.isConfigurationChangeAcceptable(updatedConfiguration,
+                                                         unacceptableReasons),
+               String.valueOf(unacceptableReasons));
+
+    ConfigChangeResult changeResult =
+         validator.applyConfigurationChange(updatedConfiguration);
+    assertEquals(changeResult.getResultCode(), ResultCode.SUCCESS);
+
+    assertFalse(validator.passwordIsAcceptable(password,
+                               new HashSet<ByteString>(0), modifyOperation,
+                               userEntry, invalidReason));
+
+    validator.finalizePasswordValidator();
+  }
+}
+

--
Gitblit v1.10.0