mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

neil_a_wilson
26.36.2007 a172bd1c78dc92255574e05c5b63cb9bcc81a26c
opendj-sdk/opends/resource/config/config.ldif
@@ -1098,6 +1098,15 @@
ds-cfg-minimum-password-length: 6
ds-cfg-maximum-password-length: 0
dn: cn=Similarity-Based Password Validator,cn=Password Validators,cn=config
objectClass: top
objectClass: ds-cfg-password-validator
objectClass: ds-cfg-similarity-based-password-validator
cn: Similarity-Based Password Validator
ds-cfg-password-validator-class: org.opends.server.extensions.SimilarityBasedPasswordValidator
ds-cfg-password-validator-enabled: true
ds-cfg-minimum-password-difference: 3
dn: cn=Plugins,cn=config
objectClass: top
objectClass: ds-cfg-branch
opendj-sdk/opends/resource/schema/02-config.ldif
@@ -1087,6 +1087,10 @@
  NAME 'ds-cfg-changelog-purge-delay'
  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.321
  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' )
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 )
@@ -1502,4 +1506,8 @@
  STRUCTURAL MUST ( ds-cfg-certificate-fingerprint-attribute-type $
  ds-cfg-certificate-fingerprint-algorithm )
  MAY ds-cfg-certificate-user-base-dn X-ORIGIN 'OpenDS Directory Server' )
objectClasses: ( 1.3.6.1.4.1.26027.1.2.88
  NAME 'ds-cfg-similarity-based-password-validator'
  SUP ds-cfg-password-validator STRUCTURAL
  MUST ds-cfg-minimum-password-difference X-ORIGIN 'OpenDS Directory Server' )
opendj-sdk/opends/src/admin/defn/org/opends/server/admin/std/SimilarityBasedPasswordValidatorConfiguration.xml
New file
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<adm:managed-object name="similarity-based-password-validator"
  plural-name="similarity-based-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
    whether the number of characters it contains falls within an acceptable
    range of values.
  </adm:synopsis>
  <adm:profile name="ldap">
    <ldap:object-class>
      <ldap:oid>1.3.6.1.4.1.26027.1.2.88</ldap:oid>
      <ldap:name>ds-cfg-similarity-based-password-validator</ldap:name>
      <ldap:superior>ds-cfg-password-validator</ldap:superior>
    </ldap:object-class>
  </adm:profile>
  <adm:property name="minimum-password-difference" mandatory="true">
    <adm:synopsis>
      Specifies the minimum difference of new and old password.
    </adm:synopsis>
    <adm:description>
      Specifies the minimal difference of new and old password.
      A value of zero indicates that there will be no difference is acceptable.
      Changes to this configuration attribute will take effect immediately.
    </adm:description>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>1</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <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.321</ldap:oid>
        <ldap:name>ds-cfg-minimum-password-difference</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
</adm:managed-object>
opendj-sdk/opends/src/server/org/opends/server/config/ConfigConstants.java
@@ -1419,6 +1419,12 @@
  public static final String ATTR_PASSWORD_MIN_LENGTH =
       NAME_PREFIX_CFG + "minimum-password-length";
  /**
   * The name of the configuration attribute that specifies the minimum allowed
   * difference for a password.
   */
  public static final String ATTR_PASSWORD_MIN_DIFFERENCE =
       NAME_PREFIX_CFG + "minimum-password-difference";
  /**
opendj-sdk/opends/src/server/org/opends/server/extensions/SimilarityBasedPasswordValidator.java
New file
@@ -0,0 +1,162 @@
/*
 * 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.List;
import java.util.Set;
import org.opends.server.api.PasswordValidator;
import org.opends.server.config.ConfigException;
import org.opends.server.core.Operation;
import org.opends.server.types.ByteString;
import org.opends.server.types.ByteStringFactory;
import org.opends.server.types.ConfigChangeResult;
import org.opends.server.types.Entry;
import org.opends.server.types.InitializationException;
import org.opends.server.types.ResultCode;
import org.opends.server.util.LevenshteinDistance;
import org.opends.server.admin.std.server.SimilarityBasedPasswordValidatorCfg;
import org.opends.server.admin.server.ConfigurationChangeListener;
import static org.opends.server.config.ConfigConstants.*;
import static org.opends.server.loggers.Error.*;
import static org.opends.server.messages.ExtensionsMessages.*;
import static org.opends.server.messages.MessageHandler.*;
import static org.opends.server.util.StaticUtils.*;
/**
 * This class provides a password validator that can ensure that the provided
 * password meets minimum similarity requirements.
 */
public class SimilarityBasedPasswordValidator extends
    PasswordValidator<SimilarityBasedPasswordValidatorCfg> implements
    ConfigurationChangeListener<SimilarityBasedPasswordValidatorCfg>
{
  // The current configuration for this password validator.
  private SimilarityBasedPasswordValidatorCfg currentConfig;
  /**
   * Creates a new instance of this password validator.
   */
  public SimilarityBasedPasswordValidator()
  {
    super();
    // All initialization must be done in the initializePasswordValidator
    // method.
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public void initializePasswordValidator(
                   SimilarityBasedPasswordValidatorCfg configuration)
         throws ConfigException, InitializationException
  {
    configuration.addSimilarityBasedChangeListener(this);
    currentConfig = configuration;
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public void finalizePasswordValidator()
  {
    currentConfig.removeSimilarityBasedChangeListener(this);
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean passwordIsAcceptable(ByteString newPassword,
                                      Set<ByteString> currentPasswords,
                                      Operation operation, Entry userEntry,
                                      StringBuilder invalidReason)  {
    int minDifference = currentConfig.getMinimumPasswordDifference();
    ByteString passwd = newPassword == null
                        ? ByteStringFactory.create("")
                        : newPassword;
    if (currentPasswords == null || currentPasswords.size() == 0) {
      // This validator requires access to at least one current password.
      // If we don't have a current password, then we can't validate it, so
      // we'll have to assume it is OK.  Ideally, the password policy should be
      // configured to always require the current password, but even then the
      // current password probably won't be availble during an administrative
      // password reset.
      return true;
    }
    for (ByteString bs : currentPasswords) {
        if (bs == null) {
            continue;
        }
        int ldistance = LevenshteinDistance.calculate(passwd.stringValue(),
                                                      bs.stringValue());
        if (ldistance < minDifference) {
          invalidReason.append(getMessage(MSGID_PWDIFFERENCEVALIDATOR_TOO_SMALL,
                                          minDifference));
          return false;
        }
    }
    return true;
  }
  /**
   * {@inheritDoc}
   */
  public boolean isConfigurationChangeAcceptable(
                      SimilarityBasedPasswordValidatorCfg configuration,
                      List<String> unacceptableReasons)
  {
    return true;
  }
  /**
   * {@inheritDoc}
   */
  public ConfigChangeResult applyConfigurationChange(
              SimilarityBasedPasswordValidatorCfg configuration)
  {
    currentConfig = configuration;
    return new ConfigChangeResult(ResultCode.SUCCESS, false);
  }
}
opendj-sdk/opends/src/server/org/opends/server/messages/ExtensionsMessages.java
@@ -4767,6 +4767,44 @@
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_SEVERE_ERROR | 452;
  /**
   * The message ID for the message that will be used as the description of the
   * minimum difference configuration attribute.  It does not take any
   * arguments.
   */
  public static final int
       MSGID_PWDIFFERENCEVALIDATOR_DESCRIPTION_MIN_DIFFERENCE =
            CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 453;
  /**
   * The message ID for the message that will be used if an error occurs while
   * attempting to determine the minimum password difference.  This takes a
   * single argument, which is a string representation of the exception that was
   * caught.
   */
  public static final int
       MSGID_PWDIFFERENCEVALIDATOR_CANNOT_DETERMINE_MIN_DIFFERENCE =
            CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 454;
  /**
   * The message ID for the message that will be used to indicate that the
   * minimum password length has been updated.  This takes a single argument,
   * which is the new minimum length.
   */
  public static final int MSGID_PWDIFFERENCEVALIDATOR_UPDATED_MIN_DIFFERENCE =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_INFORMATIONAL | 455;
  /**
   * The message ID for the message that will be used if a provided password is
   * too short.  This takes a single argument, which is the minimum required
   * password length.
   */
  public static final int MSGID_PWDIFFERENCEVALIDATOR_TOO_SMALL =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 456;
  /**
   * Associates a set of generic messages with the message IDs defined in this
@@ -6599,6 +6637,21 @@
                    "The minimum password length has been updated to %d.");
    registerMessage(MSGID_PWLENGTHVALIDATOR_UPDATED_MAX_LENGTH,
                    "The maximum password length has been updated to %d.");
    registerMessage(MSGID_PWDIFFERENCEVALIDATOR_DESCRIPTION_MIN_DIFFERENCE,
                    "Specifies the minimum difference that a " +
                    "password will be allowed to have.  A value of zero " +
                    "indicates that there is no minimum difference.  Changes " +
                    "to this configuration attribute will take effect " +
                    "immediately.");
    registerMessage(MSGID_PWDIFFERENCEVALIDATOR_CANNOT_DETERMINE_MIN_DIFFERENCE,
                    "An error occurred while attempting to determine the " +
                    "minimum allowed password difference from the " +
                    ATTR_PASSWORD_MIN_DIFFERENCE + " attribute:  %s.");
    registerMessage(MSGID_PWDIFFERENCEVALIDATOR_TOO_SMALL,
                    "The provided password differs less than the minimum " +
                    "required difference of %d characters.");
    registerMessage(MSGID_PWDIFFERENCEVALIDATOR_UPDATED_MIN_DIFFERENCE,
                    "The minimum password difference has been updated to %d.");
    registerMessage(MSGID_RANDOMPWGEN_DESCRIPTION_CHARSET,
opendj-sdk/opends/src/server/org/opends/server/util/LevenshteinDistance.java
New file
@@ -0,0 +1,154 @@
/*
 * 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.util;
import static org.opends.server.util.Validator.*;
/**
 * This class provides an implementation of the Levenshtein distance algorithm,
 * which may be used to determine the minimum number of changes required to
 * transform one string into another.  For the purpose of this algorithm, a
 * change is defined as replacing one character with another, inserting a new
 * character, or removing an existing character.
 * <BR><BR>
 * The basic algorithm works as follows for a source string S of length X and
 * a target string T of length Y:
 * <OL>
 *   <LI>Create a matrix M with dimensions of X+1, Y+1.</LI>
 *   <LI>Fill the first row with sequentially-increasing values 0 through
 *       X.</LI>
 *   <LI>Fill the first column with sequentially-increasing values 0 through
 *       Y.</LI>
 *   <LI>Create a nested loop iterating over the characters in the strings.  In
 *       the outer loop, iterate through characters in S from 0 to X-1 using an
 *       iteration counter I.  In the inner loop, iterate through the characters
 *       in T from 0 to Y-1 using an iterator counter J.  Calculate the
 *       following three things and place the smallest value in the matrix in
 *       row I+1 column J+1:
 *     <UL>
 *       <LI>One more than the value in the matrix position immediately to the
 *           left (i.e., 1 + M[I][J+1]).</LI>
 *       <LI>One more than the value in the matrix position immediately above
 *           (i.e., 1 + M[I+1][J]).</LI>
 *       <LI>Define a value V to be zero if S[I] is the same as T[J], or one if
 *           they are different.  Add that value to the value in the matrix
 *           position immediately above and to the left (i.e.,
 *           V + M[I][J]).</LI>
 *     </UL>
 *   </LI>
 *   <LI>The Levenshtein distance value, which is the least number of changes
 *       needed to transform the source string into the target string, will be
 *       the value in the bottom right corner of the matrix (i.e.,
 *       M[X][Y]).</LI>
 * </OL>
 * <BR><BR>
 * Note that this is a completely "clean room" implementation, developed from a
 * description of the algorithm, rather than copying an existing implementation.
 * Doing it in this way eliminates copyright and licensing concerns associated
 * with using an existing implementation.
 */
public final class LevenshteinDistance
{
  /**
   * Calculates the Levenshtein distance between the provided string values.
   *
   * @param  source  The source string to compare.  It must not be {@code null}.
   * @param  target  The target string to compare.  It must not be {@code null}.
   *
   * @return  The minimum number of changes required to turn the source string
   *          into the target string.
   */
  public static int calculate(String source, String target)
  {
    ensureNotNull(source, target);
    // sl == source length; tl == target length
    int sl = source.length();
    int tl = target.length();
    // If either of the lengths is zero, then the distance is the length of the
    // other string.
    if (sl == 0)
    {
      return tl;
    }
    else if (tl == 0)
    {
      return sl;
    }
    // w == matrix width; h == matrix height
    int w = sl+1;
    int h = tl+1;
    // m == matrix array
    // Create the array and fill it with values 0..sl in the first dimension and
    // 0..tl in the second dimension.
    int[][] m = new int[w][h];
    for (int i=0; i < w; i++)
    {
      m[i][0] = i;
    }
    for (int i=1; i < h; i++)
    {
      m[0][i] = i;
    }
    for (int i=0,x=1; i < sl; i++,x++)
    {
      char s = source.charAt(i);
      for (int j=0,y=1; j < tl; j++,y++)
      {
        char t = target.charAt(j);
        // Figure out what to put in the appropriate matrix slot.  It should be
        // the lowest of:
        // - One more than the value to the left
        // - One more than the value to the top
        // - If the characters are equal, the value to the upper left, otherwise
        //   one more than the value to the upper left.
        m[x][y] = Math.min(Math.min((m[i][y] + 1), (m[x][j] + 1)),
                           (m[i][j] + ((s == t) ? 0 : 1)));
      }
    }
    // The Levenshtein distance should now be the value in the lower right
    // corner of the matrix.
    return m[sl][tl];
  }
}
opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LengthBasedPasswordValidatorTestCase.java
@@ -217,7 +217,7 @@
  /**
   * Retrieves a set of invvalid configuration entries.
   * Retrieves a set of invalid configuration entries.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/SimilarityBasedPasswordValidatorTestCase.java
New file
@@ -0,0 +1,386 @@
/*
 * 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.config.ConfigException;
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.ByteStringFactory;
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 static org.testng.Assert.*;
import org.opends.server.admin.std.meta.SimilarityBasedPasswordValidatorCfgDefn;
import org.opends.server.admin.std.server.SimilarityBasedPasswordValidatorCfg;
import org.opends.server.admin.server.AdminTestCaseUtils;
/**
 * A set of test cases for the Similarity-Based Password Validator.
 */
public class SimilarityBasedPasswordValidatorTestCase
       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=Similarity-Based Password Validator,cn=Password Validators," +
              "cn=config",
         "objectClass: top",
         "objectClass: ds-cfg-password-validator",
         "objectClass: ds-cfg-similarity-based-password-validator",
         "cn: Similarity-Based Password Validator",
         "ds-cfg-password-validator-class: org.opends.server.extensions." +
              "SimilarityBasedPasswordValidator",
         "ds-cfg-password-validator-enabled: true",
         "ds-cfg-minimum-password-difference: 6",
         "",
         "dn: cn=Similarity-Based Password Validator,cn=Password Validators," +
              "cn=config",
         "objectClass: top",
         "objectClass: ds-cfg-password-validator",
         "objectClass: ds-cfg-similarity-based-password-validator",
         "cn: Similarity-Based Password Validator",
         "ds-cfg-password-validator-class: org.opends.server.extensions." +
              "SimilarityBasedPasswordValidator",
         "ds-cfg-password-validator-enabled: true",
         "ds-cfg-minimum-password-difference: 3",
         "",
         "dn: cn=Similarity-Based Password Validator,cn=Password Validators," +
              "cn=config",
         "objectClass: top",
         "objectClass: ds-cfg-password-validator",
         "objectClass: ds-cfg-similarity-based-password-validator",
         "cn: Similarity-Based Password Validator",
         "ds-cfg-password-validator-class: org.opends.server.extensions." +
              "SimilarityBasedPasswordValidator",
         "ds-cfg-password-validator-enabled: true",
         "ds-cfg-minimum-password-difference: 0"
         );
    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
  {
    SimilarityBasedPasswordValidatorCfg configuration =
         AdminTestCaseUtils.getConfiguration(
              SimilarityBasedPasswordValidatorCfgDefn.getInstance(),
              e);
    SimilarityBasedPasswordValidator validator = new SimilarityBasedPasswordValidator();
    validator.initializePasswordValidator(configuration);
  }
  /**
   * Retrieves a set of invvalid configuration entries.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @DataProvider(name = "invalidConfigs")
  public Object[][] getInvalidConfigs()
         throws Exception
  {
    List<Entry> entries = TestCaseUtils.makeEntries(
         "dn: cn=Similarity-Based Password Validator,cn=Password Validators," +
              "cn=config",
         "objectClass: top",
         "objectClass: ds-cfg-password-validator",
         "objectClass: ds-cfg-similarity-based-password-validator",
         "cn: Similarity-Based Password Validator",
         "ds-cfg-password-validator-class: org.opends.server.extensions." +
              "SimilarityBasedPasswordValidator",
         "ds-cfg-password-validator-enabled: true",
         "ds-cfg-minimum-password-difference: -1",
         "",
         "dn: cn=Similarity-Based Password Validator,cn=Password Validators," +
              "cn=config",
         "objectClass: top",
         "objectClass: ds-cfg-password-validator",
         "objectClass: ds-cfg-similarity-based-password-validator",
         "cn: Similarity-Based Password Validator",
         "ds-cfg-password-validator-class: org.opends.server.extensions." +
              "SimilarityBasedPasswordValidator",
         "ds-cfg-password-validator-enabled: true",
         // "ds-cfg-minimum-password-difference: -1", // error here
         "",
         "dn: cn=Similarity-Based Password Validator,cn=Password Validators," +
              "cn=config",
         "objectClass: top",
         "objectClass: ds-cfg-password-validator",
         "objectClass: ds-cfg-similarity-based-password-validator",
         "cn: Similarity-Based Password Validator",
         "ds-cfg-password-validator-class: org.opends.server.extensions." +
              "SimilarityBasedPasswordValidator",
         "ds-cfg-password-validator-enabled: true",
         "ds-cfg-minimum-password-difference: notNumeric");
    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
  {
    SimilarityBasedPasswordValidatorCfg configuration =
         AdminTestCaseUtils.getConfiguration(
              SimilarityBasedPasswordValidatorCfgDefn.getInstance(),
              e);
    SimilarityBasedPasswordValidator validator = new SimilarityBasedPasswordValidator();
    validator.initializePasswordValidator(configuration);
  }
  /**
   * Tests the <CODE>passwordIsAcceptable</CODE> method with no constraints on
   * password difference.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test()
  public void testPasswordIsAcceptableNoConstraints()
         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: password");
    Entry validatorEntry = TestCaseUtils.makeEntry(
         "dn: cn=Similarity-Based Password Validator,cn=Password Validators," +
              "cn=config",
         "objectClass: top",
         "objectClass: ds-cfg-password-validator",
         "objectClass: ds-cfg-similarity-based-password-validator",
         "cn: Similarity-Based Password Validator",
         "ds-cfg-password-validator-class: org.opends.server.extensions." +
              "SimilarityBasedPasswordValidator",
         "ds-cfg-password-validator-enabled: true",
         "ds-cfg-minimum-password-difference: 0"
         );
    SimilarityBasedPasswordValidatorCfg configuration =
         AdminTestCaseUtils.getConfiguration(
              SimilarityBasedPasswordValidatorCfgDefn.getInstance(),
              validatorEntry);
    SimilarityBasedPasswordValidator validator =
         new SimilarityBasedPasswordValidator();
    validator.initializePasswordValidator(configuration);
    StringBuilder buffer = new StringBuilder();
    for (int i=0; i < 20; i++)
    {
      buffer.append('x');
      ASN1OctetString password = new ASN1OctetString(buffer.toString());
      ArrayList<Modification> mods = new ArrayList<Modification>();
      mods.add(new Modification(ModificationType.REPLACE,
                                new Attribute("userpassword",
                                              buffer.toString())));
      InternalClientConnection conn =
           InternalClientConnection.getRootConnection();
      ModifyOperation op =
           new ModifyOperation(conn, conn.nextOperationID(),
                               conn.nextMessageID(), new ArrayList<Control>(),
                               DN.decode("cn=uid=test.user,o=test"), mods);
      StringBuilder invalidReason = new StringBuilder();
      assertTrue(validator.passwordIsAcceptable(password,
                                                new HashSet<ByteString>(0),
                                                op, userEntry, invalidReason));
    }
    validator.finalizePasswordValidator();
  }
  /**
   * Tests the <CODE>passwordIsAcceptable</CODE> method with a constraint on the
   * minimum password difference.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test()
  public void testPasswordIsAcceptableMinDifferenceConstraint()
         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: password");
    Entry validatorEntry = TestCaseUtils.makeEntry(
         "dn: cn=Similarity-Based Password Validator,cn=Password Validators," +
              "cn=config",
         "objectClass: top",
         "objectClass: ds-cfg-password-validator",
         "objectClass: ds-cfg-similarity-based-password-validator",
         "cn: Similarity-Based Password Validator",
         "ds-cfg-password-validator-class: org.opends.server.extensions." +
              "SimilarityBasedPasswordValidator",
         "ds-cfg-password-validator-enabled: true",
         "ds-cfg-minimum-password-difference: 3"
         );
    SimilarityBasedPasswordValidatorCfg configuration =
         AdminTestCaseUtils.getConfiguration(
              SimilarityBasedPasswordValidatorCfgDefn.getInstance(),
              validatorEntry);
    SimilarityBasedPasswordValidator validator =
         new SimilarityBasedPasswordValidator();
    validator.initializePasswordValidator(configuration);
    StringBuilder buffer = new StringBuilder();
    HashSet<ByteString> currentPassword = new HashSet<ByteString>(3);
    currentPassword.add(ByteStringFactory.create("xxx"));
    for (int i=0; i < 7; i++)
    {
      buffer.append('x');
      ASN1OctetString password = new ASN1OctetString(buffer.toString());
      ArrayList<Modification> mods = new ArrayList<Modification>();
      mods.add(new Modification(ModificationType.REPLACE,
                                new Attribute("userpassword",
                                              buffer.toString())));
      InternalClientConnection conn =
           InternalClientConnection.getRootConnection();
      ModifyOperation op =
           new ModifyOperation(conn, conn.nextOperationID(),
                               conn.nextMessageID(), new ArrayList<Control>(),
                               DN.decode("cn=uid=test.user,o=test"), mods);
      StringBuilder invalidReason = new StringBuilder();
      assertEquals((buffer.length() >= 6),
                   validator.passwordIsAcceptable(password,
                                                  currentPassword,
                                                  op, userEntry,
                                                  invalidReason));
    }
    validator.finalizePasswordValidator();
  }
}
opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/util/LevenshteinDistanceTestCase.java
New file
@@ -0,0 +1,239 @@
/*
 * 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.util;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static org.testng.Assert.*;
/**
 * A set of generic test cases for the Levenshtein distance class.
 */
public class LevenshteinDistanceTestCase
       extends UtilTestCase
{
  /**
   * Retrieves a set of data that may be used to test the Levenshtein distance
   * implementation.  Each element of the array returned will itself be an
   * array whose elements are a source string, a target string, and the
   * expected Levenshtein distance.
   *
   * @return  A set of data that may be used to test the Levenshtein distance
   *          implementation.
   */
  @DataProvider(name = "teststrings")
  public Object[][] getTestStrings()
  {
    return new Object[][]
    {
      // When the values are the same, the distance is zero.
      new Object[] { "", "", 0 },
      new Object[] { "1", "1", 0 },
      new Object[] { "12", "12", 0 },
      new Object[] { "123", "123", 0 },
      new Object[] { "1234", "1234", 0 },
      new Object[] { "12345", "12345", 0 },
      new Object[] { "password", "password", 0 },
      // When one of the values is empty, the distance is the length of the
      // other value.
      new Object[] { "", "1", 1 },
      new Object[] { "", "12", 2 },
      new Object[] { "", "123", 3 },
      new Object[] { "", "1234", 4 },
      new Object[] { "", "12345", 5 },
      new Object[] { "", "password", 8 },
      new Object[] { "1", "", 1 },
      new Object[] { "12", "", 2 },
      new Object[] { "123", "", 3 },
      new Object[] { "1234", "", 4 },
      new Object[] { "12345", "", 5 },
      new Object[] { "password", "", 8 },
      // Whenever a single character is inserted or removed, the distance is
      // one.
      new Object[] { "password", "1password", 1 },
      new Object[] { "password", "p1assword", 1 },
      new Object[] { "password", "pa1ssword", 1 },
      new Object[] { "password", "pas1sword", 1 },
      new Object[] { "password", "pass1word", 1 },
      new Object[] { "password", "passw1ord", 1 },
      new Object[] { "password", "passwo1rd", 1 },
      new Object[] { "password", "passwor1d", 1 },
      new Object[] { "password", "password1", 1 },
      new Object[] { "password", "assword", 1 },
      new Object[] { "password", "pssword", 1 },
      new Object[] { "password", "pasword", 1 },
      new Object[] { "password", "pasword", 1 },
      new Object[] { "password", "passord", 1 },
      new Object[] { "password", "passwrd", 1 },
      new Object[] { "password", "passwod", 1 },
      new Object[] { "password", "passwor", 1 },
      // Whenever a single character is replaced, the distance is one.
      new Object[] { "password", "Xassword", 1 },
      new Object[] { "password", "pXssword", 1 },
      new Object[] { "password", "paXsword", 1 },
      new Object[] { "password", "pasXword", 1 },
      new Object[] { "password", "passXord", 1 },
      new Object[] { "password", "passwXrd", 1 },
      new Object[] { "password", "passwoXd", 1 },
      new Object[] { "password", "passworX", 1 },
      // If characters are taken off the front and added to the back and all of
      // the characters are unique, then the distance is two times the number of
      // characters shifted, until you get halfway (and then it becomes easier
      // to shift from the other direction).
      new Object[] { "12345678", "23456781", 2 },
      new Object[] { "12345678", "34567812", 4 },
      new Object[] { "12345678", "45678123", 6 },
      new Object[] { "12345678", "56781234", 8 },
      new Object[] { "12345678", "67812345", 6 },
      new Object[] { "12345678", "78123456", 4 },
      new Object[] { "12345678", "81234567", 2 },
      // If all the characters are unique and the values are reversed, then the
      // distance is the number of characters for an even number of characters,
      // and one less for an odd number of characters (since the middle
      // character will stay the same).
      new Object[] { "12", "21", 2 },
      new Object[] { "123", "321", 2 },
      new Object[] { "1234", "4321", 4 },
      new Object[] { "12345", "54321", 4 },
      new Object[] { "123456", "654321", 6 },
      new Object[] { "1234567", "7654321", 6 },
      new Object[] { "12345678", "87654321", 8 },
      // The rest of these are miscellaneous interesting examples.  They will
      // be illustrated using the following key:
      // = (the characters are equal)
      // + (the character is inserted)
      // - (the character is removed)
      // # (the character is replaced)
      // Mississippi
      //  ippississiM
      // -=##====##=+ --> 6
      new Object[] { "Mississippi", "ippississiM", 6 },
      // eieio
      // oieie
      // #===# --> 2
      new Object[] { "eieio", "oieie", 2 },
      // brad+angelina
      // bra   ngelina
      // ===+++======= --> 3
      new Object[] { "brad+angelina", "brangelina", 3 },
      // test international chars
      // ?e?uli?ka
      //  e?uli?ka
      // -======== --> 1
      new Object[] { "?e?uli?ka", "e?uli?ka", 1 },
    };
  }
  /**
   * Tests the {@code calculate} method with non-{@code null} String arguments.
   *
   * @param  s  The source string to compare.
   * @param  t  The target string to compare.
   * @param  d  The expected Levenshtein distance for the two strings.
   */
  @Test(dataProvider = "teststrings")
  public void testCalculateStrings(String s, String t, int d)
  {
    assertEquals(LevenshteinDistance.calculate(s, t), d);
  }
  /**
   * Tests the {@code calculate} method with non-{@code null} String arguments
   * in reverse order to verify that they are order-independent.
   *
   * @param  s  The source string to compare.
   * @param  t  The target string to compare.
   * @param  d  The expected Levenshtein distance for the two strings.
   */
  @Test(dataProvider = "teststrings")
  public void testCalculateStringsReversed(String s, String t, int d)
  {
    assertEquals(LevenshteinDistance.calculate(t, s), d);
  }
  /**
   * Retrieves a set of data that may be used to test the Levenshtein distance
   * implementation.  Each element of the array returned will itself be an
   * array whose elements are a source string, a target string, and the
   * expected Levenshtein distance.
   *
   * @return  A set of data that may be used to test the Levenshtein distance
   *          implementation.
   */
  @DataProvider(name = "testnulls")
  public Object[][] getTestNulls()
  {
    return new Object[][]
    {
      new Object[] { "notnull", null },
      new Object[] { null, "notnull" },
      new Object[] { null, null }
    };
  }
  /**
   * Tests the {@code calculate} method with at least one {@code null} string.
   *
   * @param  s  The source string to compare.
   * @param  t  The target string to compare.
   */
  @Test(dataProvider = "testnulls",
        expectedExceptions = { AssertionError.class })
  public void testNullStrings(String s, String t)
  {
    LevenshteinDistance.calculate(s, t);
  }
}