Add a new password validator that determines whether a proposed new password
is acceptable based on how similar it is to the user's current password. The
processing uses the Levenshtein Distance algorithm to determine the number of
changes required to convert the current password into the new password (a
change may be either inserting a new character, removing an existing character,
or replacing an existing character).
Contributed By: Ales Novak
OpenDS Issue Number: 340
5 files added
5 files modified
| | |
| | | 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 |
| | |
| | | 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 ) |
| | |
| | | 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' ) |
| | | |
| New file |
| | |
| | | <?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> |
| | | |
| | |
| | | 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"; |
| | | |
| | | |
| | | /** |
| New file |
| | |
| | | /* |
| | | * 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); |
| | | } |
| | | } |
| | | |
| | |
| | | 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 |
| | |
| | | "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, |
| New file |
| | |
| | | /* |
| | | * 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]; |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | |
| | | /** |
| | | * Retrieves a set of invvalid configuration entries. |
| | | * Retrieves a set of invalid configuration entries. |
| | | * |
| | | * @throws Exception If an unexpected problem occurs. |
| | | */ |
| New file |
| | |
| | | /* |
| | | * 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(); |
| | | } |
| | | } |
| | | |
| New file |
| | |
| | | /* |
| | | * 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); |
| | | } |
| | | } |
| | | |