UnboundID Server SDK

Ping Identity
UnboundID Server SDK Documentation

ExamplePasswordValidator.java

/*
 * 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
 * docs/licenses/cddl.txt
 * or http://www.opensource.org/licenses/cddl1.php.
 * 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
 * docs/licenses/cddl.txt.  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 2010-2023 Ping Identity Corporation
 */
package com.unboundid.directory.sdk.examples;



import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.unboundid.directory.sdk.common.types.Entry;
import com.unboundid.directory.sdk.common.types.OperationContext;
import com.unboundid.directory.sdk.ds.api.PasswordValidator;
import com.unboundid.directory.sdk.ds.config.PasswordValidatorConfig;
import com.unboundid.directory.sdk.ds.types.DirectoryServerContext;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldap.sdk.unboundidds.extensions.PasswordQualityRequirement;
import com.unboundid.util.ByteString;
import com.unboundid.util.args.ArgumentException;
import com.unboundid.util.args.ArgumentParser;
import com.unboundid.util.args.StringArgument;



/**
 * This class provides a simple example of a password validator that may be used
 * to ensure that the proposed password does not match the value of a specified
 * set of attributes in the user's entry.  It has one configuration argument:
 * <UL>
 *   <LI>attribute -- The name(s) of the attributes that should be checked.
 *       If multiple attributes should be checked, then this argument should be
 *       provided multiple times with different attribute names.  If no
 *       attribute names are provided, then all user attributes in the entry
 *       will be checked.</LI>
 * </UL>
 */
public final class ExamplePasswordValidator
       extends PasswordValidator
{
  /**
   * The name of the argument that will be used to specify the attribute(s) that
   * will be checked.
   */
  private static final String ARG_NAME_ATTRIBUTE = "attribute";



  // The server context for the server in which this extension is running.
  private DirectoryServerContext serverContext;

  // The set of attributes to be checked.
  private volatile List<String> attributes;



  /**
   * Creates a new instance of this password validator.  All password validator
   * implementations must include a default constructor, but any initialization
   * should generally be done in the {@code initializePasswordValidator} method.
   */
  public ExamplePasswordValidator()
  {
    // No implementation required.
  }



  /**
   * Retrieves a human-readable name for this extension.
   *
   * @return  A human-readable name for this extension.
   */
  @Override()
  public String getExtensionName()
  {
    return "Example Password Validator";
  }



  /**
   * Retrieves a human-readable description for this extension.  Each element
   * of the array that is returned will be considered a separate paragraph in
   * generated documentation.
   *
   * @return  A human-readable description for this extension, or {@code null}
   *          or an empty array if no description should be available.
   */
  @Override()
  public String[] getExtensionDescription()
  {
    return new String[]
    {
      "This password validator serves an example that may be used to " +
           "demonstrate the process for creating a third-party password " +
           "validator.  It will reject any attempt to use a password that " +
           "matches the value of a specified set of attributes (or any " +
           "attribute) in the user's entry."
    };
  }



  /**
   * Updates the provided argument parser to define any configuration arguments
   * which may be used by this password generator.  The argument parser may also
   * be updated to define relationships between arguments (e.g., to specify
   * required, exclusive, or dependent argument sets).
   *
   * @param  parser  The argument parser to be updated with the configuration
   *                 arguments which may be used by this password generator.
   *
   * @throws  ArgumentException  If a problem is encountered while updating the
   *                             provided argument parser.
   */
  @Override()
  public void defineConfigArguments(final ArgumentParser parser)
         throws ArgumentException
  {
    // Add an argument that allows you to specify the set of target attributes.
    Character shortIdentifier = null;
    String    longIdentifier  = ARG_NAME_ATTRIBUTE;
    boolean   required        = false;
    int       maxOccurrences  = 0; // Unlimited.
    String    placeholder     = "{attr}";
    String    description     = "The name or OID of an attribute to check.";

    parser.addArgument(new StringArgument(shortIdentifier, longIdentifier,
         required, maxOccurrences, placeholder, description));
  }



  /**
   * Initializes this password validator.
   *
   * @param  serverContext  A handle to the server context for the server in
   *                        which this extension is running.
   * @param  config         The general configuration for this password
   *                        validator.
   * @param  parser         The argument parser which has been initialized from
   *                        the configuration for this password validator.
   *
   * @throws  LDAPException  If a problem occurs while initializing this
   *                         password validator.
   */
  @Override()
  public void initializePasswordValidator(
                   final DirectoryServerContext serverContext,
                   final PasswordValidatorConfig config,
                   final ArgumentParser parser)
         throws LDAPException
  {
    serverContext.debugInfo("Beginning password validator initialization");

    this.serverContext = serverContext;

    // The work we need to do is the same for the initial configuration as for
    // a configuration change, so we'll just call the same method in both cases.
    applyConfig(parser);
  }



  /**
   * Indicates whether the configuration contained in the provided argument
   * parser represents a valid configuration for this extension.
   *
   * @param  config               The general configuration for this password
   *                              validator.
   * @param  parser               The argument parser which has been initialized
   *                              with the proposed configuration.
   * @param  unacceptableReasons  A list that can be updated with reasons that
   *                              the proposed configuration is not acceptable.
   *
   * @return  {@code true} if the proposed configuration is acceptable, or
   *          {@code false} if not.
   */
  @Override()
  public boolean isConfigurationAcceptable(final PasswordValidatorConfig config,
                      final ArgumentParser parser,
                      final List<String> unacceptableReasons)
  {
    // No special validation is required.
    return true;
  }



  /**
   * Attempts to apply the configuration contained in the provided argument
   * parser.
   *
   * @param  config                The general configuration for this password
   *                               validator.
   * @param  parser                The argument parser which has been
   *                               initialized with the new configuration.
   * @param  adminActionsRequired  A list that can be updated with information
   *                               about any administrative actions that may be
   *                               required before one or more of the
   *                               configuration changes will be applied.
   * @param  messages              A list that can be updated with information
   *                               about the result of applying the new
   *                               configuration.
   *
   * @return  A result code that provides information about the result of
   *          attempting to apply the configuration change.
   */
  @Override()
  public ResultCode applyConfiguration(final PasswordValidatorConfig config,
                                       final ArgumentParser parser,
                                       final List<String> adminActionsRequired,
                                       final List<String> messages)
  {
    // The work we need to do is the same for the initial configuration as for
    // a configuration change, so we'll just call the same method in both cases.
    applyConfig(parser);

    return ResultCode.SUCCESS;
  }



  /**
   * Applies the configuration contained in the provided argument parser.
   *
   * @param  parser  The argument parser with the configuration to apply.
   */
  private void applyConfig(final ArgumentParser parser)
  {
    List<String> attrs = null;

    final StringArgument attrArg =
         (StringArgument) parser.getNamedArgument(ARG_NAME_ATTRIBUTE);
    if (attrArg != null)
    {
      attrs = attrArg.getValues();
    }

    if ((attrs == null) || attrs.isEmpty())
    {
      attributes = null;
    }
    else
    {
      attributes = attrs;
    }

    serverContext.debugInfo("Set the target attribute set to " + attributes);
  }



  /**
   * Performs any cleanup which may be necessary when this password validator is
   * to be taken out of service.
   */
  @Override()
  public void finalizePasswordValidator()
  {
    // No finalization is required.
  }



  /**
   * Indicates whether this password validator should be invoked for add
   * operations that attempt to create an entry containing one or more
   * password values.
   *
   * @return  {@code true} if this password validator should be invoked for
   *          add operations that include one or more passwords, or
   *          {@code false} if not.
   */
  @Override()
  public boolean invokeForAdd()
  {
    return true;
  }



  /**
   * Indicates whether this password validator should be invoked for modify or
   * password modify operations that represent a user's attempt to change
   * his/her own password.
   *
   * @return  {@code true} if this password validator should be invoked for
   *          self password change operations, or {@code false} if not.
   */
  @Override()
  public boolean invokeForSelfChange()
  {
    return true;
  }



  /**
   * Indicates whether this password validator should be invoked for modify or
   * password modify operations that represent one user's attempt to change the
   * password for another user.
   *
   * @return  {@code true} if this password validator should be invoked for
   *          administrative password reset operations, or {@code false} if not.
   */
  @Override()
  public boolean invokeForAdministrativeReset()
  {
    return true;
  }



  /**
   * Retrieves the password quality requirement for this password validator, if
   * available.
   *
   * @return  The password quality requirement for this password validator, or
   *          {@code null} if no requirement information is available.
   */
  @Override()
  public PasswordQualityRequirement getPasswordQualityRequirement()
  {
    final String description;
    final LinkedHashMap<String,String> clientSideValidationProperties =
         new LinkedHashMap<String,String>(10);
    if ((attributes == null) || attributes.isEmpty())
    {
      description = "The password must not match the value of any attribute " +
           "in the user's entry.";
    }
    else
    {
      int attrIdentifier = 1;
      final StringBuilder attrList = new StringBuilder();
      final Iterator<String> iterator = attributes.iterator();
      while (iterator.hasNext())
      {
        final String attrName = iterator.next();
        clientSideValidationProperties.put("attribute-" + attrIdentifier,
             attrName);
        attrIdentifier++;

        attrList.append(attrName);
        if (iterator.hasNext())
        {
          attrList.append(", ");
        }
      }

      description = "The password must not match the value of any of the " +
           "following attributes in the user's entry:  " + attrList + '.';
    }


    final String clientSideValidationType = "example-attribute-value-validator";
    return new PasswordQualityRequirement(description, clientSideValidationType,
         clientSideValidationProperties);
  }



  /**
   * Indicates whether the proposed password is acceptable for the specified
   * user.
   *
   * @param  operationContext  The operation context for the associated request.
   *                           It may be associated with an add, modify, or
   *                           password modify operation.
   * @param  newPassword       The proposed new password for the user that
   *                           should be validated.  It will not be encoded or
   *                           obscured in any way.
   * @param  currentPasswords  The current set of passwords for the user, if
   *                           available.  It may be {@code null} if this is
   *                           not available.  Note that even if one or more
   *                           current passwords are available, it may not
   *                           constitute the complete set of passwords for the
   *                           user.
   * @param  userEntry         The entry for the user whose password is being
   *                           changed.
   * @param  invalidReason     A buffer to which a message may be appended to
   *                           indicate why the proposed password is not
   *                           acceptable.
   *
   * @return  {@code true} if the proposed new password is acceptable, or
   *          {@code false} if not.
   */
  @Override()
  public boolean isPasswordAcceptable(final OperationContext operationContext,
                                      final ByteString newPassword,
                                      final Set<ByteString> currentPasswords,
                                      final Entry userEntry,
                                      final StringBuilder invalidReason)
  {
    // Create a local copy for the attribute set to protect against
    // configuration changes while performing the validation.
    final List<String> attrs = attributes;

    final byte[] newPW = newPassword.getValue();
    if (attrs == null)
    {
      // We should check all attributes in the user entry.
      for (final Attribute a : userEntry.getAttributes())
      {
        if (a.getBaseName().equalsIgnoreCase("userPassword") ||
            a.getBaseName().equalsIgnoreCase("authPassword"))
        {
          // Don't check the password attribute.
          continue;
        }

        if (a.hasValue(newPW))
        {
          invalidReason.append("The password matches the value of another " +
               "attribute in the user entry.");
          return false;
        }
      }
    }
    else
    {
      // We should check only the specified set of attributes.
      for (final String attrName : attrs)
      {
        if (userEntry.hasAttributeValue(attrName, newPW))
        {
          invalidReason.append("The password matches the value of another " +
               "attribute in the user entry.");
          return false;
        }
      }
    }

    return true;
  }



  /**
   * Retrieves a map containing examples of configurations that may be used for
   * this extension.  The map key should be a list of sample arguments, and the
   * corresponding value should be a description of the behavior that will be
   * exhibited by the extension when used with that configuration.
   *
   * @return  A map containing examples of configurations that may be used for
   *          this extension.  It may be {@code null} or empty if there should
   *          not be any example argument sets.
   */
  @Override()
  public Map<List<String>,String> getExamplesArgumentSets()
  {
    final LinkedHashMap<List<String>,String> exampleMap =
         new LinkedHashMap<List<String>,String>(2);

    exampleMap.put(
         Arrays.asList(ARG_NAME_ATTRIBUTE + "=uid",
                       ARG_NAME_ATTRIBUTE + "=givenName",
                       ARG_NAME_ATTRIBUTE + "=sn",
                       ARG_NAME_ATTRIBUTE + "=mail"),
         "Reject user passwords which match the user ID, first name, last " +
              "name, or e-mail address from the user's entry.");

    exampleMap.put(Arrays.<String>asList(),
         "Reject user passwords which match the value of any attribute in " +
              "the user's entry.");

    return exampleMap;
  }
}