/* * 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 * * * Copyright 2010-2019 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; } }