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

neil_a_wilson
11.00.2007 fbd01f29f11e1819495668158fe53cf586745343
Add support for two new password validators:

- Issue #338: Prevent users from selecting a password that matches the value
of any attribute (or a specified set of attributes) in that user's entry.

- Issue #341: Prevent users from selecting a password that matches a value
contained in a dictionary.

Both validators support both forward and reverse matching, and for the
dictionary password validator I have compiled a dictionary from public domain
word lists.
7 files added
3 files modified
428058 ■■■■■ changed files
opendj-sdk/opends/resource/config/config.ldif 20 ●●●●● patch | view | raw | blame | history
opendj-sdk/opends/resource/config/wordlist.txt 426282 ●●●●● patch | view | raw | blame | history
opendj-sdk/opends/resource/schema/02-config.ldif 30 ●●●● patch | view | raw | blame | history
opendj-sdk/opends/src/admin/defn/org/opends/server/admin/std/AttributeValuePasswordValidatorConfiguration.xml 61 ●●●●● patch | view | raw | blame | history
opendj-sdk/opends/src/admin/defn/org/opends/server/admin/std/DictionaryPasswordValidatorConfiguration.xml 80 ●●●●● patch | view | raw | blame | history
opendj-sdk/opends/src/server/org/opends/server/extensions/AttributeValuePasswordValidator.java 190 ●●●●● patch | view | raw | blame | history
opendj-sdk/opends/src/server/org/opends/server/extensions/DictionaryPasswordValidator.java 296 ●●●●● patch | view | raw | blame | history
opendj-sdk/opends/src/server/org/opends/server/messages/ExtensionsMessages.java 145 ●●●●● patch | view | raw | blame | history
opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/AttributeValuePasswordValidatorTestCase.java 435 ●●●●● patch | view | raw | blame | history
opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/DictionaryPasswordValidatorTestCase.java 519 ●●●●● patch | view | raw | blame | history
opendj-sdk/opends/resource/config/config.ldif
@@ -1088,6 +1088,26 @@
objectClass: ds-cfg-branch
cn: Password Validators
dn: cn=Attribute Value,cn=Password Validators,cn=config
objectClass: top
objectClass: ds-cfg-password-validator
objectClass: ds-cfg-attribute-value-password-validator
cn: Attribute Value
ds-cfg-password-validator-class: org.opends.server.extensions.AttributeValuePasswordValidator
ds-cfg-password-validator-enabled: true
ds-cfg-test-reversed-password: true
dn: cn=Dictionary,cn=Password Validators,cn=config
objectClass: top
objectClass: ds-cfg-password-validator
objectClass: ds-cfg-dictionary-password-validator
cn: Dictionary
ds-cfg-password-validator-class: org.opends.server.extensions.DictionaryPasswordValidator
ds-cfg-password-validator-enabled: false
ds-cfg-dictionary-file: config/wordlist.txt
ds-cfg-case-sensitive-validation: false
ds-cfg-test-reversed-password: true
dn: cn=Length-Based Password Validator,cn=Password Validators,cn=config
objectClass: top
objectClass: ds-cfg-password-validator
opendj-sdk/opends/resource/config/wordlist.txt
New file
Diff too large
opendj-sdk/opends/resource/schema/02-config.ldif
@@ -1088,20 +1088,20 @@
  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
  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
  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
  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
  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' )
attributeTypes: ( 1.3.6.1.4.1.26027.1.1.325
  NAME 'ds-cfg-virtual-attribute-class' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
@@ -1125,6 +1125,12 @@
  NAME 'ds-cfg-virtual-attribute-conflict-behavior'
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE
  X-ORIGIN 'OpenDS Directory Server' )
attributeTypes: ( 1.3.6.1.4.1.26027.1.1.336
  NAME 'ds-cfg-dictionary-file' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
  SINGLE-VALUE X-ORIGIN 'OpenDS Directory Server' )
attributeTypes: ( 1.3.6.1.4.1.26027.1.1.337
  NAME 'ds-cfg-test-reversed-password' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
  SINGLE-VALUE X-ORIGIN 'OpenDS Directory Server' )
attributeTypes: ( 1.3.6.1.4.1.26027.1.1.340
  NAME 'ds-task-rebuild-base-dn'
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE
@@ -1572,6 +1578,14 @@
  ds-cfg-virtual-attribute-conflict-behavior )
  MAY ( ds-cfg-virtual-attribute-base-dn $ ds-cfg-virtual-attribute-group-dn $
  ds-cfg-virtual-attribute-filter ) X-ORIGIN 'OpenDS Directory Server' )
objectClasses: ( 1.3.6.1.4.1.26027.1.2.95
  NAME 'ds-cfg-dictionary-password-validator' SUP ds-cfg-password-validator
  STRUCTURAL MUST ( ds-cfg-dictionary-file $ ds-cfg-case-sensitive-validation $
  ds-cfg-test-reversed-password ) X-ORIGIN 'OpenDS Directory Server' )
objectClasses: ( 1.3.6.1.4.1.26027.1.2.96
  NAME 'ds-cfg-attribute-value-password-validator'
  SUP ds-cfg-password-validator STRUCTURAL MUST ds-cfg-test-reversed-password
  MAY ds-cfg-match-attribute X-ORIGIN 'OpenDS Directory Server' )
objectClasses: ( 1.3.6.1.4.1.26027.1.2.98
  NAME 'ds-task-rebuild' SUP ds-task
  MUST ( ds-task-rebuild-base-dn $ ds-task-rebuild-index )
opendj-sdk/opends/src/admin/defn/org/opends/server/admin/std/AttributeValuePasswordValidatorConfiguration.xml
New file
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<adm:managed-object name="attribute-value-password-validator"
plural-name="attribute-value-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 given password value appears the user's entry.
  </adm:synopsis>
  <adm:profile name="ldap">
    <ldap:object-class>
      <ldap:oid>1.3.6.1.4.1.26027.1.2.95</ldap:oid>
      <ldap:name>ds-cfg-attribute-value-password-validator</ldap:name>
      <ldap:superior>ds-cfg-password-validator</ldap:superior>
    </ldap:object-class>
  </adm:profile>
  <adm:property name="match-attribute" mandatory="false" multi-valued="true">
    <adm:synopsis>
      Specifies the name(s) of the attribute(s) whose values should be checked
      to determine whether they match the provided password.  If this is not
      provided, then all attributes in the user's entry will be checked.
    </adm:synopsis>
    <adm:default-behavior>
      <adm:alias>
        <adm:synopsis>
          All attributes in the user entry will be checked.
        </adm:synopsis>
      </adm:alias>
    </adm:default-behavior>
    <adm:syntax>
      <adm:attribute-type />
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:oid>1.3.6.1.4.1.26027.1.1.146</ldap:oid>
        <ldap:name>ds-cfg-match-attribute</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property name="test-reversed-password" mandatory="true">
    <adm:synopsis>
      Indicates whether this password validator should test the reversed value
      of the provided password as well as the order in which it was given.
    </adm:synopsis>
    <adm:syntax>
      <adm:boolean />
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:oid>1.3.6.1.4.1.26027.1.1.337</ldap:oid>
        <ldap:name>ds-cfg-test-reversed-password</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
</adm:managed-object>
opendj-sdk/opends/src/admin/defn/org/opends/server/admin/std/DictionaryPasswordValidatorConfiguration.xml
New file
@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<adm:managed-object name="dictionary-password-validator"
  plural-name="dictionary-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 given password value appears in a provided dictionary file.
  </adm:synopsis>
  <adm:profile name="ldap">
    <ldap:object-class>
      <ldap:oid>1.3.6.1.4.1.26027.1.2.94</ldap:oid>
      <ldap:name>ds-cfg-dictionary-password-validator</ldap:name>
      <ldap:superior>ds-cfg-password-validator</ldap:superior>
    </ldap:object-class>
  </adm:profile>
  <adm:property name="dictionary-file" mandatory="true">
    <adm:synopsis>
      Specifies the path to the file containing a list of words that may not be
      used as passwords.  It should be formatted with one word per line.  The
      value may be an absolute path, or a path that is relative to the
      <adm:product-name />
      instance root.
    </adm:synopsis>
    <adm:syntax>
      <adm:string />
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:oid>1.3.6.1.4.1.26027.1.1.336</ldap:oid>
        <ldap:name>ds-cfg-dictionary-file</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:property name="test-reversed-password" mandatory="true">
    <adm:synopsis>
      Indicates whether this password validator should test the reversed value
      of the provided password as well as the order in which it was given.
    </adm:synopsis>
    <adm:syntax>
      <adm:boolean />
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:oid>1.3.6.1.4.1.26027.1.1.337</ldap:oid>
        <ldap:name>ds-cfg-test-reversed-password</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
</adm:managed-object>
opendj-sdk/opends/src/server/org/opends/server/extensions/AttributeValuePasswordValidator.java
New file
@@ -0,0 +1,190 @@
/*
 * 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.AttributeValuePasswordValidatorCfg;
import org.opends.server.api.PasswordValidator;
import org.opends.server.core.Operation;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeType;
import org.opends.server.types.AttributeValue;
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 contained in another attribute in the user's
 * entry.
 */
public class AttributeValuePasswordValidator
       extends PasswordValidator<AttributeValuePasswordValidatorCfg>
       implements ConfigurationChangeListener<
                       AttributeValuePasswordValidatorCfg>
{
  // The current configuration for this password validator.
  private AttributeValuePasswordValidatorCfg currentConfig;
  /**
   * Creates a new instance of this attribute value password validator.
   */
  public AttributeValuePasswordValidator()
  {
    super();
    // No implementation is required here.  All initialization should be
    // performed in the initializePasswordValidator() method.
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public void initializePasswordValidator(
                   AttributeValuePasswordValidatorCfg configuration)
  {
    configuration.addAttributeValueChangeListener(this);
    currentConfig = configuration;
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public void finalizePasswordValidator()
  {
    currentConfig.removeAttributeValueChangeListener(this);
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean passwordIsAcceptable(ByteString newPassword,
                                      Set<ByteString> currentPasswords,
                                      Operation operation, Entry userEntry,
                                      StringBuilder invalidReason)
  {
    // Get a handle to the current configuration.
    AttributeValuePasswordValidatorCfg config = currentConfig;
    // Get the string representation (both forward and reversed) for the
    // password.
    String password = newPassword.stringValue();
    String reversed = new StringBuilder(password).reverse().toString();
    // If we should check a specific set of attributes, then do that now.
    // Otherwise, check all user attributes.
    Set<AttributeType> matchAttributes = config.getMatchAttribute();
    if ((matchAttributes == null) || matchAttributes.isEmpty())
    {
      matchAttributes = userEntry.getUserAttributes().keySet();
    }
    for (AttributeType t : matchAttributes)
    {
      List<Attribute> attrList = userEntry.getAttribute(t);
      if ((attrList == null) || attrList.isEmpty())
      {
        continue;
      }
      AttributeValue vf = new AttributeValue(t, password);
      AttributeValue vr = new AttributeValue(t, reversed);
      for (Attribute a : attrList)
      {
        if (a.hasValue(vf) ||
            (config.isTestReversedPassword() && a.hasValue(vr)))
        {
          int msgID = MSGID_ATTRVALUE_VALIDATOR_PASSWORD_IN_ENTRY;
          invalidReason.append(getMessage(msgID));
          return false;
        }
      }
    }
    // If we've gotten here, then the password is acceptable.
    return true;
  }
  /**
   * {@inheritDoc}
   */
  public boolean isConfigurationChangeAcceptable(
                      AttributeValuePasswordValidatorCfg configuration,
                      List<String> unacceptableReasons)
  {
    // If we've gotten this far, then we'll accept the change.
    return true;
  }
  /**
   * {@inheritDoc}
   */
  public ConfigChangeResult applyConfigurationChange(
                      AttributeValuePasswordValidatorCfg configuration)
  {
    ResultCode        resultCode          = ResultCode.SUCCESS;
    boolean           adminActionRequired = false;
    ArrayList<String> messages            = new ArrayList<String>();
    currentConfig = configuration;
    return new ConfigChangeResult(resultCode, adminActionRequired, messages);
  }
}
opendj-sdk/opends/src/server/org/opends/server/extensions/DictionaryPasswordValidator.java
New file
@@ -0,0 +1,296 @@
/*
 * 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.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
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.DictionaryPasswordValidatorCfg;
import org.opends.server.api.PasswordValidator;
import org.opends.server.config.ConfigException;
import org.opends.server.core.Operation;
import org.opends.server.types.ConfigChangeResult;
import org.opends.server.types.ByteString;
import org.opends.server.types.DebugLogLevel;
import org.opends.server.types.DirectoryConfig;
import org.opends.server.types.Entry;
import org.opends.server.types.InitializationException;
import org.opends.server.types.ResultCode;
import static org.opends.server.loggers.debug.DebugLogger.*;
import static org.opends.server.messages.ExtensionsMessages.*;
import static org.opends.server.messages.MessageHandler.*;
import static org.opends.server.util.StaticUtils.*;
/**
 * This class provides an OpenDS password validator that may be used to ensure
 * that proposed passwords are not contained in a specified dictionary.
 */
public class DictionaryPasswordValidator
       extends PasswordValidator<DictionaryPasswordValidatorCfg>
       implements ConfigurationChangeListener<DictionaryPasswordValidatorCfg>
{
  // The current configuration for this password validator.
  private DictionaryPasswordValidatorCfg currentConfig;
  // The current dictionary that we should use when performing the validation.
  private HashSet<String> dictionary;
  /**
   * Creates a new instance of this dictionary password validator.
   */
  public DictionaryPasswordValidator()
  {
    super();
    // No implementation is required here.  All initialization should be
    // performed in the initializePasswordValidator() method.
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public void initializePasswordValidator(
                   DictionaryPasswordValidatorCfg configuration)
         throws ConfigException, InitializationException
  {
    configuration.addDictionaryChangeListener(this);
    currentConfig = configuration;
    dictionary = loadDictionary(configuration);
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public void finalizePasswordValidator()
  {
    currentConfig.removeDictionaryChangeListener(this);
  }
  /**
   * {@inheritDoc}
   */
  @Override()
  public boolean passwordIsAcceptable(ByteString newPassword,
                                      Set<ByteString> currentPasswords,
                                      Operation operation, Entry userEntry,
                                      StringBuilder invalidReason)
  {
    // Get a handle to the current configuration.
    DictionaryPasswordValidatorCfg config = currentConfig;
    HashSet<String> dictionary = this.dictionary;
    // Check to see if the provided password is in the dictionary in the order
    // that it was provided.
    String password = newPassword.stringValue();
    if (! config.isCaseSensitiveValidation())
    {
      password = toLowerCase(password);
    }
    if (dictionary.contains(password))
    {
      int msgID = MSGID_DICTIONARY_VALIDATOR_PASSWORD_IN_DICTIONARY;
      invalidReason.append(getMessage(msgID));
      return false;
    }
    // If we should try the reversed value, then do that as well.
    if (config.isTestReversedPassword())
    {
      if (dictionary.contains(new StringBuilder(password).reverse().toString()))
      {
        int msgID = MSGID_DICTIONARY_VALIDATOR_PASSWORD_IN_DICTIONARY;
        invalidReason.append(getMessage(msgID));
        return false;
      }
    }
    // If we've gotten here, then the password is acceptable.
    return true;
  }
  /**
   * Loads the configured dictionary and returns it as a hash set.
   *
   * @param  configuration  the configuration for this password validator.
   *
   * @return  The hash set containing the loaded dictionary data.
   *
   * @throws  ConfigException  If the configured dictionary file does not exist.
   *
   * @throws  InitializationException  If a problem occurs while attempting to
   *                                   read from the dictionary file.
   */
  private HashSet<String> loadDictionary(
                               DictionaryPasswordValidatorCfg configuration)
          throws ConfigException, InitializationException
  {
    // Get the path to the dictionary file and make sure it exists.
    File dictionaryFile = getFileForPath(configuration.getDictionaryFile());
    if (! dictionaryFile.exists())
    {
      int    msgID   = MSGID_DICTIONARY_VALIDATOR_NO_SUCH_FILE;
      String message = getMessage(msgID, configuration.getDictionaryFile());
      throw new ConfigException(msgID, message);
    }
    // Read the contents of file into the dictionary as per the configuration.
    BufferedReader reader = null;
    HashSet<String> dictionary = new HashSet<String>();
    try
    {
      reader = new BufferedReader(new FileReader(dictionaryFile));
      String line = reader.readLine();
      while (line != null)
      {
        if (! configuration.isCaseSensitiveValidation())
        {
          line = line.toLowerCase();
        }
        dictionary.add(line);
        line = reader.readLine();
      }
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        debugCaught(DebugLogLevel.ERROR, e);
      }
      int    msgID   = MSGID_DICTIONARY_VALIDATOR_CANNOT_READ_FILE;
      String message = getMessage(msgID, configuration.getDictionaryFile(),
                                  String.valueOf(e));
      throw new InitializationException(msgID, message);
    }
    finally
    {
      if (reader != null)
      {
        try
        {
          reader.close();
        } catch (Exception e) {}
      }
    }
    return dictionary;
  }
  /**
   * {@inheritDoc}
   */
  public boolean isConfigurationChangeAcceptable(
                      DictionaryPasswordValidatorCfg configuration,
                      List<String> unacceptableReasons)
  {
    // Make sure that we can load the dictionary.  If so, then we'll accept the
    // new configuration.
    try
    {
      loadDictionary(configuration);
    }
    catch (ConfigException ce)
    {
      unacceptableReasons.add(ce.getMessage());
      return false;
    }
    catch (InitializationException ie)
    {
      unacceptableReasons.add(ie.getMessage());
      return false;
    }
    catch (Exception e)
    {
      unacceptableReasons.add(stackTraceToSingleLineString(e));
      return false;
    }
    return true;
  }
  /**
   * {@inheritDoc}
   */
  public ConfigChangeResult applyConfigurationChange(
                      DictionaryPasswordValidatorCfg configuration)
  {
    ResultCode        resultCode          = ResultCode.SUCCESS;
    boolean           adminActionRequired = false;
    ArrayList<String> messages            = new ArrayList<String>();
    // Make sure we can load the dictionary.  If we can, then activate the new
    // configuration.
    try
    {
      dictionary    = loadDictionary(configuration);
      currentConfig = configuration;
    }
    catch (Exception e)
    {
      resultCode = DirectoryConfig.getServerErrorResultCode();
      messages.add(stackTraceToSingleLineString(e));
    }
    return new ConfigChangeResult(resultCode, adminActionRequired, messages);
  }
}
opendj-sdk/opends/src/server/org/opends/server/messages/ExtensionsMessages.java
@@ -4835,6 +4835,111 @@
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 459;
  /**
   * The message ID for the message that will be used if the provided password
   * is contained in the server's dictionary.  This does not take any arguments.
   */
  public static final int MSGID_DICTIONARY_VALIDATOR_PASSWORD_IN_DICTIONARY =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 460;
  /**
   * The message ID for the message that will be used if the provided password
   * dictionary file does not exist.  This takes a single argument, which is the
   * path to the dictionary file.
   */
  public static final int MSGID_DICTIONARY_VALIDATOR_NO_SUCH_FILE =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 461;
  /**
   * The message ID for the message that will be used if an error occurs while
   * trying to read the password file.  This takes two arguments, which is the
   * path to the dictionary file and a string representation of the exception
   * that was caught.
   */
  public static final int MSGID_DICTIONARY_VALIDATOR_CANNOT_READ_FILE =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 462;
  /**
   * The message ID for the message that will be used if the provided password
   * is contained in another attribute in the user's entry.  This does not take
   * any arguments.
   */
  public static final int MSGID_ATTRVALUE_VALIDATOR_PASSWORD_IN_ENTRY =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 463;
  /**
   * The message ID for the message that will be used if the provided password
   * contains a character not included in any of the defined character sets.
   * This takes a single argument, which is the illegal character.
   */
  public static final int MSGID_CHARSET_VALIDATOR_ILLEGAL_CHARACTER =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 464;
  /**
   * The message ID for the message that will be used if the provided password
   * contains too few passwords from a given character set.  This takes two
   * arguments, which is a string of the characters from that set and the
   * minimum number of characters from that set which must be used in passwords.
   */
  public static final int MSGID_CHARSET_VALIDATOR_TOO_FEW_CHARS_FROM_SET =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 465;
  /**
   * The message ID for the message that will be used if the provided character
   * set definition does not contain a colon to separate the count from the
   * character set.  This takes a single argument, which is the provided
   * definition string.
   */
  public static final int MSGID_CHARSET_VALIDATOR_NO_COLON =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 466;
  /**
   * The message ID for the message that will be used if the provided character
   * set definition does not contain any characters after the colon.  This takes
   * a single argument, which is the provided definition string.
   */
  public static final int MSGID_CHARSET_VALIDATOR_NO_CHARS =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 467;
  /**
   * The message ID for the message that will be used if the provided character
   * set definition is invalid because the value before the colon could not be
   * parsed as positive integer.  This takes a single argument, which is the
   * provided definition string.
   */
  public static final int MSGID_CHARSET_VALIDATOR_INVALID_COUNT =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 468;
  /**
   * The message ID for the message that will be used if the provided character
   * set definition contains a character that has already been used.  This takes
   * two arguments, which are the provided definition string and the duplicate
   * character.
   */
  public static final int MSGID_CHARSET_VALIDATOR_DUPLICATE_CHAR =
       CATEGORY_MASK_EXTENSIONS | SEVERITY_MASK_MILD_ERROR | 469;
  /**
   * Associates a set of generic messages with the message IDs defined in this
   * class.
@@ -6964,6 +7069,46 @@
    registerMessage(MSGID_SUBSCHEMASUBENTRY_VATTR_NOT_SEARCHABLE,
                    "The %s attribute is not searchable and should not be " +
                    "included in otherwise unindexed search filters.");
    registerMessage(MSGID_DICTIONARY_VALIDATOR_PASSWORD_IN_DICTIONARY,
                    "The provided password was found in the server's " +
                    "dictionary.");
    registerMessage(MSGID_DICTIONARY_VALIDATOR_NO_SUCH_FILE,
                    "The specified dictionary file %s does not exist.");
    registerMessage(MSGID_DICTIONARY_VALIDATOR_CANNOT_READ_FILE,
                    "An error occurred while attempting to load the " +
                    "dictionary from file %s:  %s.");
    registerMessage(MSGID_ATTRVALUE_VALIDATOR_PASSWORD_IN_ENTRY,
                    "The provided password was found in another attribute " +
                    "in the user entry.");
    registerMessage(MSGID_CHARSET_VALIDATOR_ILLEGAL_CHARACTER,
                    "The provided password contained character '%s' which is " +
                    "not allowed for use in passwords.");
    registerMessage(MSGID_CHARSET_VALIDATOR_TOO_FEW_CHARS_FROM_SET,
                    "The provided password did not contain enough " +
                    "characters from the character set '%s'.  The minimum " +
                    "number of characters from that set that must be present " +
                    "in user passwords is %d.");
    registerMessage(MSGID_CHARSET_VALIDATOR_NO_COLON,
                    "The provided character set definition '%s' is invalid " +
                    "because it does not contain a colon to separate the " +
                    "minimum count from the character set.");
    registerMessage(MSGID_CHARSET_VALIDATOR_NO_CHARS,
                    "The provided character set definition '%s' is invalid " +
                    "because the provided character set is empty.");
    registerMessage(MSGID_CHARSET_VALIDATOR_INVALID_COUNT,
                    "The provided character set definition '%s' is invalid " +
                    "because the value before the colon must be an integer " +
                    "greater than zero.");
    registerMessage(MSGID_CHARSET_VALIDATOR_DUPLICATE_CHAR,
                    "The provided character set definition '%s' is invalid " +
                    "because it contains character '%s' which has already " +
                    "been used.");
  }
}
opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/AttributeValuePasswordValidatorTestCase.java
New file
@@ -0,0 +1,435 @@
/*
 * 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.AttributeValuePasswordValidatorCfgDefn;
import org.opends.server.admin.std.server.AttributeValuePasswordValidatorCfg;
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 attribute value password validator.
 */
public class AttributeValuePasswordValidatorTestCase
       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=Attribute Value,cn=Password Validators,cn=config",
         "objectClass: top",
         "objectClass: ds-cfg-password-validator",
         "objectClass: ds-cfg-attribute-value-password-validator",
         "cn: Attribute Value",
         "ds-cfg-password-validator-class: org.opends.server.extensions." +
              "AttributeValuePasswordValidator",
         "ds-cfg-password-validator-enabled: true",
         "ds-cfg-test-reversed-password: true",
         "",
         "dn: cn=Attribute Value,cn=Password Validators,cn=config",
         "objectClass: top",
         "objectClass: ds-cfg-password-validator",
         "objectClass: ds-cfg-attribute-value-password-validator",
         "cn: Attribute Value",
         "ds-cfg-password-validator-class: org.opends.server.extensions." +
              "AttributeValuePasswordValidator",
         "ds-cfg-password-validator-enabled: true",
         "ds-cfg-match-attribute: uid",
         "ds-cfg-test-reversed-password: true",
         "",
         "dn: cn=Attribute Value,cn=Password Validators,cn=config",
         "objectClass: top",
         "objectClass: ds-cfg-password-validator",
         "objectClass: ds-cfg-attribute-value-password-validator",
         "cn: Attribute Value",
         "ds-cfg-password-validator-class: org.opends.server.extensions." +
              "AttributeValuePasswordValidator",
         "ds-cfg-password-validator-enabled: true",
         "ds-cfg-match-attribute: uid",
         "ds-cfg-match-attribute: cn",
         "ds-cfg-match-attribute: givenName",
         "ds-cfg-match-attribute: sn",
         "ds-cfg-test-reversed-password: true",
         "",
         "dn: cn=Attribute Value,cn=Password Validators,cn=config",
         "objectClass: top",
         "objectClass: ds-cfg-password-validator",
         "objectClass: ds-cfg-attribute-value-password-validator",
         "cn: Attribute Value",
         "ds-cfg-password-validator-class: org.opends.server.extensions." +
              "AttributeValuePasswordValidator",
         "ds-cfg-password-validator-enabled: true",
         "ds-cfg-test-reversed-password: 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", groups= { "slow" })
  public void testInitializeWithValidConfigs(Entry e)
         throws Exception
  {
    AttributeValuePasswordValidatorCfg configuration =
         AdminTestCaseUtils.getConfiguration(
              AttributeValuePasswordValidatorCfgDefn.getInstance(), e);
    AttributeValuePasswordValidator validator =
         new AttributeValuePasswordValidator();
    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(
         // Invalid test-reversed-password
         "dn: cn=Attribute Value,cn=Password Validators,cn=config",
         "objectClass: top",
         "objectClass: ds-cfg-password-validator",
         "objectClass: ds-cfg-attribute-value-password-validator",
         "cn: Attribute Value",
         "ds-cfg-password-validator-class: org.opends.server.extensions." +
              "AttributeValuePasswordValidator",
         "ds-cfg-password-validator-enabled: true",
         "ds-cfg-test-reversed-password: invalid",
         "",
         // Invalid match attribute.
         "dn: cn=Attribute Value,cn=Password Validators,cn=config",
         "objectClass: top",
         "objectClass: ds-cfg-password-validator",
         "objectClass: ds-cfg-attribute-value-password-validator",
         "cn: Attribute Value",
         "ds-cfg-password-validator-class: org.opends.server.extensions." +
              "AttributeValuePasswordValidator",
         "ds-cfg-password-validator-enabled: true",
         "ds-cfg-match-attribute: nosuchattribute",
         "ds-cfg-test-reversed-password: true");
    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
  {
    AttributeValuePasswordValidatorCfg configuration =
         AdminTestCaseUtils.getConfiguration(
              AttributeValuePasswordValidatorCfgDefn.getInstance(), e);
    AttributeValuePasswordValidator validator =
         new AttributeValuePasswordValidator();
    validator.initializePasswordValidator(configuration);
  }
  /**
   * Retrieves a set of data to use when testing a given password with a
   * provided configuration.  Each element of the returned array should be an
   * array of a configuration entry, a test password string, and an indication
   * as to whether the provided password should be acceptable.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @DataProvider(name = "testData")
  public Object[][] getTestData()
         throws Exception
  {
    return new Object[][]
    {
      // Default configuration, with a password that does not match an existing
      // attribute value.
      new Object[]
      {
        TestCaseUtils.makeEntry(
             "dn: cn=Attribute Value,cn=Password Validators,cn=config",
             "objectClass: top",
             "objectClass: ds-cfg-password-validator",
             "objectClass: ds-cfg-attribute-value-password-validator",
             "cn: Attribute Value",
             "ds-cfg-password-validator-class: org.opends.server.extensions." +
                  "AttributeValuePasswordValidator",
             "ds-cfg-password-validator-enabled: true",
             "ds-cfg-test-reversed-password: true"),
        "password",
        true
      },
      // Default configuration, with a password that matches an existing
      // attribute value.
      new Object[]
      {
        TestCaseUtils.makeEntry(
             "dn: cn=Attribute Value,cn=Password Validators,cn=config",
             "objectClass: top",
             "objectClass: ds-cfg-password-validator",
             "objectClass: ds-cfg-attribute-value-password-validator",
             "cn: Attribute Value",
             "ds-cfg-password-validator-class: org.opends.server.extensions." +
                  "AttributeValuePasswordValidator",
             "ds-cfg-password-validator-enabled: true",
             "ds-cfg-test-reversed-password: true"),
        "test",
        false
      },
      // Default configuration, with a password that matches the reverse of an
      // existing attribute value with reverwse matching enabled
      new Object[]
      {
        TestCaseUtils.makeEntry(
             "dn: cn=Attribute Value,cn=Password Validators,cn=config",
             "objectClass: top",
             "objectClass: ds-cfg-password-validator",
             "objectClass: ds-cfg-attribute-value-password-validator",
             "cn: Attribute Value",
             "ds-cfg-password-validator-class: org.opends.server.extensions." +
                  "AttributeValuePasswordValidator",
             "ds-cfg-password-validator-enabled: true",
             "ds-cfg-test-reversed-password: true"),
        "tset",
        false
      },
      // Default configuration, with a password that matches the reverse of an
      // existing attribute value with reverwse matching disabled
      new Object[]
      {
        TestCaseUtils.makeEntry(
             "dn: cn=Attribute Value,cn=Password Validators,cn=config",
             "objectClass: top",
             "objectClass: ds-cfg-password-validator",
             "objectClass: ds-cfg-attribute-value-password-validator",
             "cn: Attribute Value",
             "ds-cfg-password-validator-class: org.opends.server.extensions." +
                  "AttributeValuePasswordValidator",
             "ds-cfg-password-validator-enabled: true",
             "ds-cfg-test-reversed-password: false"),
        "tset",
        true
      },
      // Default configuration, with a password that matches one of the values
      // of a specified set of attributes.
      new Object[]
      {
        TestCaseUtils.makeEntry(
             "dn: cn=Attribute Value,cn=Password Validators,cn=config",
             "objectClass: top",
             "objectClass: ds-cfg-password-validator",
             "objectClass: ds-cfg-attribute-value-password-validator",
             "cn: Attribute Value",
             "ds-cfg-password-validator-class: org.opends.server.extensions." +
                  "AttributeValuePasswordValidator",
             "ds-cfg-password-validator-enabled: true",
             "ds-cfg-match-attribute: cn",
             "ds-cfg-match-attribute: sn",
             "ds-cfg-match-attribute: givenName",
             "ds-cfg-test-reversed-password: true"),
        "test",
        false
      },
      // Default configuration, with a password that doesn't match any of the
      // values of a specified set of attributes but does match the value of
      // another attribute in the entry.
      new Object[]
      {
        TestCaseUtils.makeEntry(
             "dn: cn=Attribute Value,cn=Password Validators,cn=config",
             "objectClass: top",
             "objectClass: ds-cfg-password-validator",
             "objectClass: ds-cfg-attribute-value-password-validator",
             "cn: Attribute Value",
             "ds-cfg-password-validator-class: org.opends.server.extensions." +
                  "AttributeValuePasswordValidator",
             "ds-cfg-password-validator-enabled: true",
             "ds-cfg-match-attribute: cn",
             "ds-cfg-match-attribute: sn",
             "ds-cfg-match-attribute: givenName",
             "ds-cfg-test-reversed-password: true"),
        "test.user",
        true
      },
    };
  }
  /**
   * Tests the {@code passwordIsAcceptable} method using the provided
   * information.
   *
   * @param  configEntry  The configuration entry to use for the password
   *                      validator.
   * @param  password     The password to test with the validator.
   * @param  acceptable   Indicates whether the provided password should be
   *                      considered acceptable.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dataProvider = "testData")
  public void testPasswordIsAcceptable(Entry configEntry, String password,
                                       boolean acceptable)
         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");
    AttributeValuePasswordValidatorCfg configuration =
         AdminTestCaseUtils.getConfiguration(
              AttributeValuePasswordValidatorCfgDefn.getInstance(),
              configEntry);
    AttributeValuePasswordValidator validator =
         new AttributeValuePasswordValidator();
    validator.initializePasswordValidator(configuration);
    ASN1OctetString pwOS = 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();
    assertEquals(validator.passwordIsAcceptable(pwOS,
                              new HashSet<ByteString>(0), modifyOperation,
                              userEntry, invalidReason),
                 acceptable, invalidReason.toString());
    validator.finalizePasswordValidator();
  }
}
opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/DictionaryPasswordValidatorTestCase.java
New file
@@ -0,0 +1,519 @@
/*
 * 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.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
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.DictionaryPasswordValidatorCfgDefn;
import org.opends.server.admin.std.server.DictionaryPasswordValidatorCfg;
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 dictionary password validator.
 */
public class DictionaryPasswordValidatorTestCase
       extends ExtensionsTestCase
{
  /**
   * The path to the dictionary file that we have created for the purposes of
   * this test case.
   */
  private static String dictionaryFile;
  /**
   * Ensures that the Directory Server is running.  Also, create a very small
   * test dictionary file to use for the test cases so we don't suffer from
   * loading the real word list every time.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @BeforeClass()
  public void startServer()
         throws Exception
  {
    TestCaseUtils.startServer();
    dictionaryFile = TestCaseUtils.createTempFile(
      "love",
      "sex",
      "secret",
      "god",
      "password"
    );
  }
  /**
   * 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=Dictionary,cn=Password Validators,cn=config",
         "objectClass: top",
         "objectClass: ds-cfg-password-validator",
         "objectClass: ds-cfg-dictionary-password-validator",
         "cn: Dictionary",
         "ds-cfg-password-validator-class: org.opends.server.extensions." +
              "DictionaryPasswordValidator",
         "ds-cfg-password-validator-enabled: true",
         "ds-cfg-dictionary-file: " + dictionaryFile,
         "ds-cfg-case-sensitive-validation: false",
         "ds-cfg-test-reversed-password: true",
         "",
         "dn: cn=Dictionary,cn=Password Validators,cn=config",
         "objectClass: top",
         "objectClass: ds-cfg-password-validator",
         "objectClass: ds-cfg-dictionary-password-validator",
         "cn: Dictionary",
         "ds-cfg-password-validator-class: org.opends.server.extensions." +
              "DictionaryPasswordValidator",
         "ds-cfg-password-validator-enabled: true",
         "ds-cfg-dictionary-file: " + dictionaryFile,
         "ds-cfg-case-sensitive-validation: true",
         "ds-cfg-test-reversed-password: true",
         "",
         "dn: cn=Dictionary,cn=Password Validators,cn=config",
         "objectClass: top",
         "objectClass: ds-cfg-password-validator",
         "objectClass: ds-cfg-dictionary-password-validator",
         "cn: Dictionary",
         "ds-cfg-password-validator-class: org.opends.server.extensions." +
              "DictionaryPasswordValidator",
         "ds-cfg-password-validator-enabled: true",
         "ds-cfg-dictionary-file: " + dictionaryFile,
         "ds-cfg-case-sensitive-validation: false",
         "ds-cfg-test-reversed-password: true");
    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
  {
    DictionaryPasswordValidatorCfg configuration =
         AdminTestCaseUtils.getConfiguration(
              DictionaryPasswordValidatorCfgDefn.getInstance(), e);
    DictionaryPasswordValidator validator =
         new DictionaryPasswordValidator();
    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(
         // Invalid dictionary file
         "dn: cn=Dictionary,cn=Password Validators,cn=config",
         "objectClass: top",
         "objectClass: ds-cfg-password-validator",
         "objectClass: ds-cfg-dictionary-password-validator",
         "cn: Dictionary",
         "ds-cfg-password-validator-class: org.opends.server.extensions." +
              "DictionaryPasswordValidator",
         "ds-cfg-password-validator-enabled: true",
         "ds-cfg-dictionary-file: invalid",
         "ds-cfg-case-sensitive-validation: false",
         "ds-cfg-test-reversed-password: true",
         "",
         // Dictionary file not a file.
         "dn: cn=Dictionary,cn=Password Validators,cn=config",
         "objectClass: top",
         "objectClass: ds-cfg-password-validator",
         "objectClass: ds-cfg-dictionary-password-validator",
         "cn: Dictionary",
         "ds-cfg-password-validator-class: org.opends.server.extensions." +
              "DictionaryPasswordValidator",
         "ds-cfg-password-validator-enabled: true",
         "ds-cfg-dictionary-file: config",
         "ds-cfg-case-sensitive-validation: false",
         "ds-cfg-test-reversed-password: true",
         "",
         // Invalid case-sensitive-validation
         "dn: cn=Dictionary,cn=Password Validators,cn=config",
         "objectClass: top",
         "objectClass: ds-cfg-password-validator",
         "objectClass: ds-cfg-dictionary-password-validator",
         "cn: Dictionary",
         "ds-cfg-password-validator-class: org.opends.server.extensions." +
              "DictionaryPasswordValidator",
         "ds-cfg-password-validator-enabled: true",
         "ds-cfg-dictionary-file: " + dictionaryFile,
         "ds-cfg-case-sensitive-validation: invalid",
         "ds-cfg-test-reversed-password: true",
         "",
         // Invalid test-reversed-password
         "dn: cn=Dictionary,cn=Password Validators,cn=config",
         "objectClass: top",
         "objectClass: ds-cfg-password-validator",
         "objectClass: ds-cfg-dictionary-password-validator",
         "cn: Dictionary",
         "ds-cfg-password-validator-class: org.opends.server.extensions." +
              "DictionaryPasswordValidator",
         "ds-cfg-password-validator-enabled: true",
         "ds-cfg-dictionary-file: " + dictionaryFile,
         "ds-cfg-case-sensitive-validation: false",
         "ds-cfg-test-reversed-password: invalid");
    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
  {
    DictionaryPasswordValidatorCfg configuration =
         AdminTestCaseUtils.getConfiguration(
              DictionaryPasswordValidatorCfgDefn.getInstance(), e);
    DictionaryPasswordValidator validator =
         new DictionaryPasswordValidator();
    validator.initializePasswordValidator(configuration);
  }
  /**
   * Retrieves a set of data to use when testing a given password with a
   * provided configuration.  Each element of the returned array should be an
   * array of a configuration entry, a test password string, and an indication
   * as to whether the provided password should be acceptable.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @DataProvider(name = "testData")
  public Object[][] getTestData()
         throws Exception
  {
    return new Object[][]
    {
      // Default configuration with a word not in the dictionary.
      new Object[]
      {
        TestCaseUtils.makeEntry(
             "dn: cn=Dictionary,cn=Password Validators,cn=config",
             "objectClass: top",
             "objectClass: ds-cfg-password-validator",
             "objectClass: ds-cfg-dictionary-password-validator",
             "cn: Dictionary",
             "ds-cfg-password-validator-class: org.opends.server.extensions." +
                  "DictionaryPasswordValidator",
             "ds-cfg-password-validator-enabled: true",
             "ds-cfg-dictionary-file: " + dictionaryFile,
             "ds-cfg-case-sensitive-validation: false",
             "ds-cfg-test-reversed-password: true"),
        "notindictionary",
        true
      },
      // Default configuration with a word in the dictionary
      new Object[]
      {
        TestCaseUtils.makeEntry(
             "dn: cn=Dictionary,cn=Password Validators,cn=config",
             "objectClass: top",
             "objectClass: ds-cfg-password-validator",
             "objectClass: ds-cfg-dictionary-password-validator",
             "cn: Dictionary",
             "ds-cfg-password-validator-class: org.opends.server.extensions." +
                  "DictionaryPasswordValidator",
             "ds-cfg-password-validator-enabled: true",
             "ds-cfg-dictionary-file: " + dictionaryFile,
             "ds-cfg-case-sensitive-validation: false",
             "ds-cfg-test-reversed-password: true"),
        "password",
        false
      },
      // Default configuration with a word in the dictionary, case-insensitive
      // matching enabled
      new Object[]
      {
        TestCaseUtils.makeEntry(
             "dn: cn=Dictionary,cn=Password Validators,cn=config",
             "objectClass: top",
             "objectClass: ds-cfg-password-validator",
             "objectClass: ds-cfg-dictionary-password-validator",
             "cn: Dictionary",
             "ds-cfg-password-validator-class: org.opends.server.extensions." +
                  "DictionaryPasswordValidator",
             "ds-cfg-password-validator-enabled: true",
             "ds-cfg-dictionary-file: " + dictionaryFile,
             "ds-cfg-case-sensitive-validation: false",
             "ds-cfg-test-reversed-password: true"),
        "PaSsWoRd",
        false
      },
      // Default configuration with a word in the dictionary, case-insensitive
      // matching disabled
      new Object[]
      {
        TestCaseUtils.makeEntry(
             "dn: cn=Dictionary,cn=Password Validators,cn=config",
             "objectClass: top",
             "objectClass: ds-cfg-password-validator",
             "objectClass: ds-cfg-dictionary-password-validator",
             "cn: Dictionary",
             "ds-cfg-password-validator-class: org.opends.server.extensions." +
                  "DictionaryPasswordValidator",
             "ds-cfg-password-validator-enabled: true",
             "ds-cfg-dictionary-file: " + dictionaryFile,
             "ds-cfg-case-sensitive-validation: true",
             "ds-cfg-test-reversed-password: true"),
        "PaSsWoRd",
        true
      },
      // Default configuration with a reverse of a word in the dictionary,
      // reversed matching enabled
      new Object[]
      {
        TestCaseUtils.makeEntry(
             "dn: cn=Dictionary,cn=Password Validators,cn=config",
             "objectClass: top",
             "objectClass: ds-cfg-password-validator",
             "objectClass: ds-cfg-dictionary-password-validator",
             "cn: Dictionary",
             "ds-cfg-password-validator-class: org.opends.server.extensions." +
                  "DictionaryPasswordValidator",
             "ds-cfg-password-validator-enabled: true",
             "ds-cfg-dictionary-file: " + dictionaryFile,
             "ds-cfg-case-sensitive-validation: false",
             "ds-cfg-test-reversed-password: true"),
        "drowssap",
        false
      },
      // Default configuration with a reverse of a word in the dictionary,
      // reversed matching disabled
      new Object[]
      {
        TestCaseUtils.makeEntry(
             "dn: cn=Dictionary,cn=Password Validators,cn=config",
             "objectClass: top",
             "objectClass: ds-cfg-password-validator",
             "objectClass: ds-cfg-dictionary-password-validator",
             "cn: Dictionary",
             "ds-cfg-password-validator-class: org.opends.server.extensions." +
                  "DictionaryPasswordValidator",
             "ds-cfg-password-validator-enabled: true",
             "ds-cfg-dictionary-file: " + dictionaryFile,
             "ds-cfg-case-sensitive-validation: false",
             "ds-cfg-test-reversed-password: false"),
        "drowssap",
        true
      },
      // Default configuration with a reverse of a word in the dictionary,
      // reversed matching enabled and case-insensitive matching enabled
      new Object[]
      {
        TestCaseUtils.makeEntry(
             "dn: cn=Dictionary,cn=Password Validators,cn=config",
             "objectClass: top",
             "objectClass: ds-cfg-password-validator",
             "objectClass: ds-cfg-dictionary-password-validator",
             "cn: Dictionary",
             "ds-cfg-password-validator-class: org.opends.server.extensions." +
                  "DictionaryPasswordValidator",
             "ds-cfg-password-validator-enabled: true",
             "ds-cfg-dictionary-file: " + dictionaryFile,
             "ds-cfg-case-sensitive-validation: false",
             "ds-cfg-test-reversed-password: true"),
        "dRoWsSaP",
        false
      },
      // Default configuration with a reverse of a word in the dictionary,
      // reversed matching enabled and case-insensitive matching disabled
      new Object[]
      {
        TestCaseUtils.makeEntry(
             "dn: cn=Dictionary,cn=Password Validators,cn=config",
             "objectClass: top",
             "objectClass: ds-cfg-password-validator",
             "objectClass: ds-cfg-dictionary-password-validator",
             "cn: Dictionary",
             "ds-cfg-password-validator-class: org.opends.server.extensions." +
                  "DictionaryPasswordValidator",
             "ds-cfg-password-validator-enabled: true",
             "ds-cfg-dictionary-file: " + dictionaryFile,
             "ds-cfg-case-sensitive-validation: true",
             "ds-cfg-test-reversed-password: true"),
        "dRoWsSaP",
        true
      },
    };
  }
  /**
   * Tests the {@code passwordIsAcceptable} method using the provided
   * information.
   *
   * @param  configEntry  The configuration entry to use for the password
   *                      validator.
   * @param  password     The password to test with the validator.
   * @param  acceptable   Indicates whether the provided password should be
   *                      considered acceptable.
   *
   * @throws  Exception  If an unexpected problem occurs.
   */
  @Test(dataProvider = "testData")
  public void testPasswordIsAcceptable(Entry configEntry, String password,
                                       boolean acceptable)
         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");
    DictionaryPasswordValidatorCfg configuration =
         AdminTestCaseUtils.getConfiguration(
              DictionaryPasswordValidatorCfgDefn.getInstance(),
              configEntry);
    DictionaryPasswordValidator validator =
         new DictionaryPasswordValidator();
    validator.initializePasswordValidator(configuration);
    ASN1OctetString pwOS = 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();
    assertEquals(validator.passwordIsAcceptable(pwOS,
                              new HashSet<ByteString>(0), modifyOperation,
                              userEntry, invalidReason),
                 acceptable, invalidReason.toString());
    validator.finalizePasswordValidator();
  }
}