/* * 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 2014-2023 Ping Identity Corporation */ package com.unboundid.directory.sdk.examples; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import com.unboundid.directory.sdk.common.operation.ExtendedRequest; import com.unboundid.directory.sdk.common.schema.AttributeType; import com.unboundid.directory.sdk.common.types.OperationContext; import com.unboundid.directory.sdk.proxy.api.ProxiedExtendedOperationHandler; import com.unboundid.directory.sdk.proxy.config. ProxiedExtendedOperationHandlerConfig; import com.unboundid.directory.sdk.proxy.types.BackendSet; import com.unboundid.directory.sdk.proxy.types.EntryBalancingRequestProcessor; import com.unboundid.directory.sdk.proxy.types.ProxyingRequestProcessor; import com.unboundid.directory.sdk.proxy.types.ProxyServerContext; import com.unboundid.ldap.sdk.DN; import com.unboundid.ldap.sdk.ExtendedResult; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.ResultCode; import com.unboundid.ldap.sdk.extensions.PasswordModifyExtendedRequest; import com.unboundid.ldap.sdk.unboundidds.controls.NoOpRequestControl; import com.unboundid.ldap.sdk.unboundidds.controls.PasswordPolicyRequestControl; import com.unboundid.ldap.sdk.unboundidds.controls.PurgePasswordRequestControl; import com.unboundid.ldap.sdk.unboundidds.controls.RetirePasswordRequestControl; import com.unboundid.ldap.sdk.unboundidds.controls. SuppressOperationalAttributeUpdateRequestControl; import com.unboundid.util.ObjectPair; import com.unboundid.util.StaticUtils; import com.unboundid.util.args.Argument; import com.unboundid.util.args.ArgumentException; import com.unboundid.util.args.ArgumentParser; import com.unboundid.util.args.StringArgument; /** * This class provides an example of a proxied extended operation handler that * can be used to forward password modify extended requests (as defined in * <A HREF="http://www.ietf.org/rfc/rfc3062.txt">RFC 3062</A>) through the * Directory Proxy Server to one or more backend servers in simple proxy and/or * entry-balanced configurations. * <BR><BR> * This proxied extended operation handler takes the following configuration * arguments: * <UL> * <LI>username-attribute -- This specifies the name of the attribute that * will be used in an attempt to locate the target entry for an * authentication identity that starts with "u:". If this is not * specified, a default of "uid" will be used. * </UL> */ public final class ExampleProxiedExtendedOperationHandler extends ProxiedExtendedOperationHandler { /** * The name of the argument that will be used to specify the username * attribute. */ private static final String ARG_NAME_USERNAME_ATTRIBUTE = "username-attribute"; /** * The default value that will be used for the username attribute. */ private static final String DEFAULT_USERNAME_ATTRIBUTE = "uid"; /** * The name of the operation attachment that may be used to hold a pre-decoded * version of the request. */ private static final String ATTACHMENT_NAME_DECODED_REQUEST = ExampleProxiedExtendedOperationHandler.class.getName() + ".decodedRequest"; /** * The name of the operation attachment that may be used to hold a pre-decoded * version of the target user DN from the request. */ private static final String ATTACHMENT_NAME_DECODED_USER_DN = ExampleProxiedExtendedOperationHandler.class.getName() + ".decodedUserDN"; /** * The name of the operation attachment that may be used to hold a pre-decoded * version of the target username from the request. */ private static final String ATTACHMENT_NAME_DECODED_USERNAME = ExampleProxiedExtendedOperationHandler.class.getName() + ".decodedUsername"; // The name of the attribute that will be used for username lookups. private volatile String usernameAttribute = DEFAULT_USERNAME_ATTRIBUTE; /** * Creates a new instance of this proxied extended operation handler. All * proxied extended operation handler implementations must include a default * constructor, but any initialization should generally be done in the * {@code initializeProxiedExtendedOperationHandler} method. */ public ExampleProxiedExtendedOperationHandler() { // No implementation is required. } /** * {@inheritDoc} */ @Override() public String getExtensionName() { return "Password Modify Proxied Extended Operation Handler"; } /** * {@inheritDoc} */ @Override() public String[] getExtensionDescription() { return new String[] { "This example is intended to demonstrate the process for creating a " + "custom proxied extended operation handler via the UnboundID " + "Server SDK. In this case, it has the ability to examine a " + "password modify extended request to determine which backend " + "server(s) should be used to process it. The userIdentity field " + "of the request will be used in conjunction with the " + "authentication identity of the underlying client connection in " + "order to determine where the request should be sent." }; } /** * {@inheritDoc} */ @Override() public void defineConfigArguments(final ArgumentParser parser) throws ArgumentException { final String usernameAttrArgDescription = "The name of the attribute " + "that will be used to look up user entries for a given username."; final StringArgument usernameAttrArg = new StringArgument( null, // The short identifier -- none will be used. ARG_NAME_USERNAME_ATTRIBUTE, // The long identifier. false, // Argument not required. 1, // It may be provided at most one time. "{attr}", // A placeholder for the argument value. usernameAttrArgDescription, // The argument description. DEFAULT_USERNAME_ATTRIBUTE); // The default value. parser.addArgument(usernameAttrArg); } /** * Initializes this proxied extended operation handler. * * @param serverContext A handle to the server context for the server in * which this extension is running. * @param config The general configuration for this extended * operation handler. * @param parser The argument parser which has been initialized from * the configuration for this extended operation * handler. * * @throws LDAPException If a problem occurs while initializing this * extended operation handler. */ @Override() public void initializeProxiedExtendedOperationHandler( final ProxyServerContext serverContext, final ProxiedExtendedOperationHandlerConfig config, final ArgumentParser parser) throws LDAPException { usernameAttribute = getUsernameAttribute(parser); } /** * Indicates whether the configuration represented by the provided argument * parser is acceptable for use by this extension. The parser will have been * used to parse any configuration available for this extension, and any * automatic validation will have been performed. This method may be used to * perform any more complex validation which cannot be performed automatically * by the argument parser. * * @param config The general configuration for this extension. * @param parser The argument parser that has been used to * parse the proposed configuration for this * extension. * @param unacceptableReasons A list to which messages may be added to * provide additional information about why the * provided configuration is not acceptable. * * @return {@code true} if the configuration in the provided argument parser * appears to be acceptable, or {@code false} if not. */ @Override() public boolean isConfigurationAcceptable( final ProxiedExtendedOperationHandlerConfig config, final ArgumentParser parser, final List<String> unacceptableReasons) { boolean acceptable = true; // If a username attribute was specified, then make sure it's defined in the // server schema. final ProxyServerContext serverContext = config.getServerContext(); final String usernameAttr = getUsernameAttribute(parser); final AttributeType attrType = serverContext.getSchema().getAttributeType(usernameAttr, false); if (attrType == null) { unacceptableReasons.add("Username attribute '" + usernameAttr + "' is not defined in the server schema."); acceptable = false; } return acceptable; } /** * Attempts to apply the configuration from the provided argument parser to * this extension. * * @param config The general configuration for this extension. * @param parser The argument parser that has been used to * parse the new configuration for this * extension. * @param adminActionsRequired A list to which messages may be added to * provide additional information about any * additional administrative actions that may * be required to apply some of the * configuration changes. * @param messages A list to which messages may be added to * provide additional information about the * processing performed by this method. * * @return A result code providing information about the result of applying * the configuration change. A result of {@code SUCCESS} should be * used to indicate that all processing completed successfully. Any * other result will indicate that a problem occurred during * processing. */ @Override() public ResultCode applyConfiguration( final ProxiedExtendedOperationHandlerConfig config, final ArgumentParser parser, final List<String> adminActionsRequired, final List<String> messages) { // Update the username attribute. We'll do this regardless of whether it // has actually changed. usernameAttribute = getUsernameAttribute(parser); return ResultCode.SUCCESS; } /** * Performs any cleanup which may be necessary when this proxied extended * operation handler is to be taken out of service. */ @Override() public void finalizeProxiedExtendedOperationHandler() { // No implementation is required. } /** * Retrieves the name of the extended operation with the provided OID. * * @param oid The OID of the extended operation for which to retrieve the * corresponding name. * * @return The name of the extended operation with the specified OID, or * {@code null} if the specified OID is not recognized by this * proxied extended operation handler. */ @Override() public String getExtendedOperationName(final String oid) { if (oid.equals(PasswordModifyExtendedRequest.PASSWORD_MODIFY_REQUEST_OID)) { return "Password Modify Extended Request"; } else { return null; } } /** * Retrieves the OIDs of the extended operation types supported by this * proxied extended operation handler. * * @return The OIDs of the extended operation types supported by this proxied * extended operation handler. It must not be {@code null} or * empty, and the contents of the set returned must not change over * the life of this proxied extended operation handler. */ @Override() public Set<String> getSupportedExtensions() { // This extended operation handler only supports the password modify // operation. final LinkedHashSet<String> oids = new LinkedHashSet<String>(1); oids.add(PasswordModifyExtendedRequest.PASSWORD_MODIFY_REQUEST_OID); return Collections.unmodifiableSet(oids); } /** * Retrieves the OIDs of any controls supported by this proxied extended * operation handler. * * @return The OIDs of any controls supported by this proxied extended * operation handler. It may be {@code null} or empty if this * proxied extended operation handler does not support any controls. */ @Override() public Set<String> getSupportedControls() { // A number of controls are supported for use with the password modify // extended operation. Note, however, that this implementation does not // supported proxied password modify in conjunction with transactions. final LinkedHashSet<String> oids = new LinkedHashSet<String>(5); oids.add(NoOpRequestControl.NO_OP_REQUEST_OID); oids.add(PasswordPolicyRequestControl.PASSWORD_POLICY_REQUEST_OID); oids.add(PurgePasswordRequestControl.PURGE_PASSWORD_REQUEST_OID); oids.add(RetirePasswordRequestControl.RETIRE_PASSWORD_REQUEST_OID); oids.add(SuppressOperationalAttributeUpdateRequestControl. SUPPRESS_OP_ATTR_UPDATE_REQUEST_OID); return Collections.unmodifiableSet(oids); } /** * Retrieves the OIDs of any features supported by this proxied extended * operation handler that should be advertised in the server root DSE. * * @return The OIDs of any features supported by this proxied extended * operation handler. It may be {@code null} or empty if this * proxied extended operation handler does not support any features. */ @Override() public Set<String> getSupportedFeatures() { // There are no additional features supported by this extension. return Collections.emptySet(); } /** * Selects the entry-balancing backend set(s) to which the provided extended * request should be forwarded. This method will only be invoked for * operations in which the requester's client connection policy includes one * or more subtree views which reference an entry-balancing request processor. * <BR><BR> * This method returns two groups of backend sets, with the first representing * an initial guess (e.g., based on information obtained from the * entry-balancing global index), and the second representing a fallback if * the initial guess was found to be incorrect. * <BR><BR> * If it can be determined that no backend sets associated with the provided * entry-balancing request processor should be used to process the extended * operation, then the object returned may have both elements set to * {@code null} or empty sets. If it is possible to definitively determine * the set of backend sets to which the operation should be forwarded and no * fallback option is required, then the first element of the returned object * should be populated with a non-empty set and the second element should be * {@code null} or empty. * <BR><BR> * For the password modify extended operation, since the request should target * a single entry, the first set may include the one backend set in which we * believe the target entry exists (based on information from the global * index), and the second set may include all other backend sets in case it * was not found in the first set. If the entry-balancing global index cannot * be used to provide any hint about the location of the entry, then the first * set will include all backend sets and the second set will be null to * indicate that there is no fallback option. * * @param operationContext The operation context for the extended operation. * @param request The extended request to be processed. * @param requestProcessor The entry-balancing request processor in which * the extended operation should be processed. * * @return The set of backend sets to which the request should be forwarded. * It may be {@code null} (or it may be an {@code ObjectPair} with * both elements empty or {@code null}) if the request should not be * forwarded to any backend set for the entry-balancing request * processor. * * @throws LDAPException If the request should not be forwarded to any of * the backend sets for the provided entry-balancing * request processor, and the result contained in the * exception should be used instead. */ @Override() public ObjectPair<Set<BackendSet>,Set<BackendSet>> selectBackendSets( final OperationContext operationContext, final ExtendedRequest request, final EntryBalancingRequestProcessor requestProcessor) throws LDAPException { // Extract the relevant details from the request. Since this extended // operation handler could be invoked multiple times for the same request // (if the Directory Proxy Server is configured to forward multiple portions // of the DIT), we'll use operation attachments to avoid decoding things // multiple times. final PasswordModifyExtendedRequest passwordModifyRequest = getPasswordModifyRequest(operationContext, request); // Although a password modify request is allowed to not include a new // password (which indicates that the server should generate the new // password on behalf of the user), we won't allow that here. The reason is // that there are cases in which the request might need to be forwarded to // multiple backend sets (especially if the targeted entry is outside the // balancing point) and there's no way to have different servers generate // the same new password. Technically we could allow this for cases in // which we know the target entry is below the balancing point, but it's // simpler to not allow this at all. if (passwordModifyRequest.getRawNewPassword() == null) { throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM, "The password modify proxied extended operation handler cannot be " + "used to proxy requests that do not specify an explicit new " + "password."); } // See if the request targets a user by DN (whether implicitly or // explicitly) or by username. final DN userDN = getTargetUserDN(operationContext, passwordModifyRequest); if (userDN == null) { // This means that the target user was specified as a username rather than // a DN. In this case, we'll always process the request in this // entry-balancing request processor, but we can still try to use the // global index to figure out where to route the request based on the // username attribute. final String username = getTargetUsername(operationContext, passwordModifyRequest); final BackendSet indexedSet = requestProcessor.getGlobalIndexHint( usernameAttribute, username); if (indexedSet == null) { // It's not in the global index, so send it everywhere. return new ObjectPair<Set<BackendSet>,Set<BackendSet>>( new LinkedHashSet<BackendSet>(requestProcessor.getBackendSets()), null); } else { // Prioritize for the case where the entry is where the global index // thinks it is, but fall back to the rest of the servers. final LinkedHashSet<BackendSet> firstGuess = new LinkedHashSet<BackendSet>(1); firstGuess.add(indexedSet); final LinkedHashSet<BackendSet> remaining = new LinkedHashSet<BackendSet>(requestProcessor.getBackendSets()); remaining.remove(indexedSet); return new ObjectPair<Set<BackendSet>,Set<BackendSet>>(firstGuess, remaining); } } else { // We have a DN, so first see if it's below the request processor. If // not, then we won't process it here at all. if (! userDN.isDescendantOf(requestProcessor.getRequestProcessorBaseDN(), true)) { return null; } // If the user DN is not below the entry-balancing base DN, then we'll // send it everywhere. if (! userDN.isDescendantOf(requestProcessor.getBalancingPointBaseDN(), false)) { return new ObjectPair<Set<BackendSet>,Set<BackendSet>>( new LinkedHashSet<BackendSet>(requestProcessor.getBackendSets()), null); } // See if we can use the global index to get a hint as to the location of // the entry. final BackendSet indexedSet = requestProcessor.getGlobalIndexHint(userDN); if (indexedSet == null) { // It's not in the global index, so send it everywhere. return new ObjectPair<Set<BackendSet>,Set<BackendSet>>( new LinkedHashSet<BackendSet>(requestProcessor.getBackendSets()), null); } else { // Prioritize for the case where the entry is where the global index // thinks it is, but fall back to the rest of the servers. final LinkedHashSet<BackendSet> firstGuess = new LinkedHashSet<BackendSet>(1); firstGuess.add(indexedSet); final LinkedHashSet<BackendSet> remaining = new LinkedHashSet<BackendSet>(requestProcessor.getBackendSets()); remaining.remove(indexedSet); return new ObjectPair<Set<BackendSet>,Set<BackendSet>>(firstGuess, remaining); } } } /** * Obtains the extended result that should be used as a result of processing * an operation in one or more entry-balanced backend sets, or throws an * exception to indicate that the request should instead be forwarded to the * fallback server set(s). * <BR><BR> * This method will only be invoked for cases in which the * {@link #selectBackendSets} method indicates that multiple backend sets * should be accessed in the course of processing an extended request. For * the password modify extended operation, this will be any case in which the * global index cannot be used, or for the fallback set of servers in the * event that the global index hint is incorrect. * * @param operationContext The operation context for the extended * operation. * @param request The extended request that was processed. * @param requestProcessor The entry-balancing request processor in which * the extended operation was processed. * @param results A list of the extended results obtained from * processing the operation in the selected set of * backend sets, with each result paired with * information about the backend set from which it * was obtained. * @param fallbackAvailable Indicates whether a fallback group of backend * sets is available and may be used as a second * attempt at processing the operation if the * result from the initial attempt is not * acceptable. * * @return An extended result that represents the merged result from the * provided list of results. It must not be {@code null}. * * @throws LDAPException To indicate that the initial set of results was not * acceptable and that the operation should instead be * forwarded to the fallback group of backend sets. * If no fallback set of results is available, then an * extended result will be generated from the content * of this exception. */ @Override() public ExtendedResult mergeEntryBalancedResults( final OperationContext operationContext, final ExtendedRequest request, final EntryBalancingRequestProcessor requestProcessor, final List<ObjectPair<ExtendedResult,BackendSet>> results, final boolean fallbackAvailable) throws LDAPException { // If there are no results at all, then that's an error. This should // never happen, but we'll check for it just in case. if (results.isEmpty()) { throw new LDAPException(ResultCode.OTHER, "No results were obtained from the entry-balancing request " + "processor with base DN '" + requestProcessor.getRequestProcessorBaseDN() + "'."); } // Look at the set of results and categorize them as either successes or // failures. final ArrayList<ObjectPair<ExtendedResult,BackendSet>> successes = new ArrayList<ObjectPair<ExtendedResult,BackendSet>>(results.size()); final ArrayList<ObjectPair<ExtendedResult,BackendSet>> failures = new ArrayList<ObjectPair<ExtendedResult,BackendSet>>(results.size()); for (final ObjectPair<ExtendedResult,BackendSet> r : results) { if (r.getFirst().getResultCode() == ResultCode.SUCCESS) { successes.add(r); } else { failures.add(r); } } // If we only got successes, then return the first success result. if (failures.isEmpty()) { return successes.get(0).getFirst(); } // If we only got failures, then we may either return an error result or // throw an exception to indicate that the fallback servers should be used. if (successes.isEmpty()) { // If an operation fails because the entry doesn't exist, then the result // code should be either NO_SUCH_OBJECT or INVALID_CREDENTIALS (if the // request included the user's current password and the server shouldn't // divulge the reason for the failure). If we find any failure with a // different result, then we'll return that result even if there are // fallback servers. for (final ObjectPair<ExtendedResult,BackendSet> r : failures) { switch (r.getFirst().getResultCode().intValue()) { case ResultCode.NO_SUCH_OBJECT_INT_VALUE: case ResultCode.INVALID_CREDENTIALS_INT_VALUE: break; default: return r.getFirst(); } } // This suggests that all the failures were because either the target // entry doesn't exist or, if a current password was provided, then the // current password was wrong. We can't tell which so we'll assume that // the entry doesn't exist. If there are fallback servers, then we'll // throw an exception. Otherwise return the first error result we got. if (fallbackAvailable) { throw new LDAPException(ResultCode.NO_RESULTS_RETURNED, "No success results were returned from the entry-balancing " + "request processor with base DN '" + requestProcessor.getRequestProcessorBaseDN() + "'. Try again with the fallback servers."); } else { return results.get(0).getFirst(); } } // If we've gotten here, then we got a mix of success and failure results. // If we can extract a target DN from the request, then use that to // determine whether we expected one or multiple successes. final PasswordModifyExtendedRequest passwordModifyRequest = getPasswordModifyRequest(operationContext, request); final DN userDN = getTargetUserDN(operationContext, passwordModifyRequest); if (userDN != null) { // If the target user DN is subordinate to the balancing point, then we // should have gotten either exactly one success or all failures. If it's // not below the balancing point, then we should have gotten either all // successes or all failures. if (userDN.isDescendantOf(requestProcessor.getBalancingPointBaseDN(), false)) { if (successes.size() == 1) { // We got the one success we expected, so we'll return it. return successes.get(0).getFirst(); } else { // We got multiple successes, which suggests the entry exists in // multiple backend sets. This is an error. final String message = "The password modify operation targeting " + "entry '" + userDN + "' (which is below the balancing point) " + "succeeded in multiple backend sets, indicating that the " + "target entry exists in multiple sets."; return generateEntryBalancingErrorResult(message, operationContext.getMessageID(), successes, failures); } } else { // The entry isn't below the balancing point, so we should have gotten // the same result everywhere. Return an error response indicating that // we didn't. final String message = "The password modify operation targeting " + "entry '" + userDN + "' (which is not below the balancing point " + "and therefore should exist in all backend sets) succeeded in " + "at least one backend set but also failed in at least one set. " + "This means that the entry is inconsistent across the backend " + "sets."; return generateEntryBalancingErrorResult(message, operationContext.getMessageID(), successes, failures); } } // If we've gotten here, then we know that the entry was targeted by // username rather than by DN, and that there were a mix of success and // failure results. If there was a single success result, then return it. // If there were multiple success results but also one or more failures, // then generate an error result. if (successes.size() == 1) { return successes.get(0).getFirst(); } else { final String username = getTargetUsername(operationContext, passwordModifyRequest); final String message = "The password modify operating targeting the " + "user with username '" + username + "' succeeded in multiple " + "backend sets but also failed in one or more sets. This means " + "that the entry is inconsistent across the backend sets."; return generateEntryBalancingErrorResult(message, operationContext.getMessageID(), successes, failures); } } /** * Generates an error result for use in entry-balancing failures. * * @param baseMessage The base message to include in the result. * @param messageID The message ID for the result. * @param successes The list of successful backend set operations. * @param failures The list of failed backend set operations. * * @return The generated entry-balancing error result. */ private static ExtendedResult generateEntryBalancingErrorResult( final String baseMessage, final int messageID, final List<ObjectPair<ExtendedResult,BackendSet>> successes, final List<ObjectPair<ExtendedResult,BackendSet>> failures) { final StringBuilder message = new StringBuilder(); message.append(baseMessage); message.append(" The operation succeeded in the following backend " + "sets: "); final Iterator<ObjectPair<ExtendedResult,BackendSet>> sIterator = successes.iterator(); while (sIterator.hasNext()) { message.append('\''); message.append(sIterator.next().getSecond().getBackendSetID()); message.append('\''); if (sIterator.hasNext()) { message.append(", "); } } message.append(". The operation failed in the following backend " + "sets: "); final Iterator<ObjectPair<ExtendedResult,BackendSet>> fIterator = failures.iterator(); while (fIterator.hasNext()) { final ObjectPair<ExtendedResult,BackendSet> p = fIterator.next(); message.append('\''); message.append(p.getSecond().getBackendSetID()); message.append("' (result code "); message.append(p.getFirst().getResultCode().intValue()); final String diagnosticMessage = p.getFirst().getDiagnosticMessage(); if (diagnosticMessage != null) { message.append(", diagnosticMessage='"); message.append(diagnosticMessage); message.append('\''); } message.append(')'); if (fIterator.hasNext()) { message.append(", "); } } message.append('.'); return new ExtendedResult(messageID, ResultCode.OTHER, message.toString(), null, null, null, null, null); } /** * Indicates whether the provided extended request should be forwarded to one * of the servers associated with the provided proxying request processor. * Note that this method will not be invoked for proxying request processors * associated with an entry-balancing request processor. * * @param operationContext The operation context for the extended operation. * @param request The extended request to be processed. * @param requestProcessor The proxying request processor for which to * make the determination. * * @return {@code true} if the extended request should be forwarded to one of * the servers associated with the proxying request processor, or * {@code false} if not. * * @throws LDAPException If the request should not be forwarded to a * backend server associated with the proxying request * processor, but the result contained in the * exception should be used instead. */ @Override() public boolean shouldForward(final OperationContext operationContext, final ExtendedRequest request, final ProxyingRequestProcessor requestProcessor) throws LDAPException { // Extract the relevant details from the request. Since this extended // operation handler could be invoked multiple times for the same request // (if the Directory Proxy Server is configured to forward multiple portions // of the DIT), we'll use operation attachments to avoid decoding things // multiple times. final PasswordModifyExtendedRequest passwordModifyRequest = getPasswordModifyRequest(operationContext, request); // Although a password modify request is allowed to not include a new // password (which indicates that the server should generate the new // password on behalf of the user), we won't allow that here. The reason is // that there are cases in which the request might need to be forwarded to // multiple request processors and there's no way to have different servers // generate the same new password. Technically we could allow this for // cases in which we know the target entry is below the balancing point, but // it's simpler to not allow this at all. if (passwordModifyRequest.getRawNewPassword() == null) { throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM, "The password modify proxied extended operation handler cannot be " + "used to proxy requests that do not specify an explicit new " + "password."); } // If the request specifies a target entry by DN, then see if it's within // the scope of this request processor. final DN userDN = getTargetUserDN(operationContext, passwordModifyRequest); if (userDN == null) { // This means that the target user was specified as a username rather than // a DN. In this case, we'll always process the request in this // proxying request processor. return true; } else { // We'll only forward the request if the target user DN is below the // request processor base DN. return userDN.isDescendantOf(requestProcessor.getRequestProcessorBaseDN(), true); } } /** * Creates the final extended result to return to the client from the provided * list of results obtained from the set of entry-balanced and/or proxying * request processors to which the request was forwarded. * * @param operationContext The operation context for the extended * operation. * @param request The extended request that was processed. * @param results The results from all request processors to which * the request was forwarded. It may be empty if * the request was not forwarded to any backend * servers, in which case this method must * construct an appropriate result. It may have * only a single element if the request was only * forwarded to one server, and in many cases it * may be desirable to simply use that result as * the final result. It may also have multiple * elements if the request was forwarded to * multiple backend servers, in which case this * method must determine whether to return one of * them to the client, or to construct a new result * to return instead. * * @return The final extended result to be returned to the client. */ @Override() public ExtendedResult createFinalResult( final OperationContext operationContext, final ExtendedRequest request, final List<ExtendedResult> results) { // If there aren't any results, then that means we didn't process the // operation in any request processor. That should only occur if the // request targeted an entry by DN and that DN was outside the base DNs of // all proxying or entry-balancing request processors. In that case, return // a NO_SUCH_OBJECT result. if (results.isEmpty()) { return new ExtendedResult(operationContext.getMessageID(), ResultCode.NO_SUCH_OBJECT, "The targeted entry does not exist within any proxied or " + "entry-balanced request processor.", null, null, null, null, null); } // Iterate over the results to figure out which (if any) to use. Categorize // them as successes or failures. final ArrayList<ExtendedResult> successes = new ArrayList<ExtendedResult>(results.size()); final ArrayList<ExtendedResult> failures = new ArrayList<ExtendedResult>(results.size()); for (final ExtendedResult r : results) { if (r.getResultCode() == ResultCode.SUCCESS) { successes.add(r); } else { failures.add(r); } } // If there was only a single success, then return it. if (successes.size() == 1) { return successes.get(0); } // If there were multiple successes, then this is an error. This should // only happen for cases in which the request targeted a user by username // and there were multiple entries in different portions of the DIT with // the same username. if (successes.size() >= 2) { String username = null; try { final PasswordModifyExtendedRequest passwordModifyRequest = getPasswordModifyRequest(operationContext, request); username = getTargetUsername(operationContext, passwordModifyRequest); } catch (final Exception e) { operationContext.getServerContext().debugCaught(e); } String message = "The password modify operation succeeded in multiple " + "request processors, which indicates that multiple entries " + "in different portions of the DIT matched the target user " + "identity and had their passwords updated."; if (username != null) { message += " You will likely want to identify all entries with " + "username '" + username + "' that are accessible via " + "entry-balancing or proxying request processors and determine " + "whether any of them has been updated in error."; } return new ExtendedResult(operationContext.getMessageID(), ResultCode.OTHER, message, null, null, null, null, null); } // If we've gotten here, then there were only failures. Iterate through // them and return the first failure we find with a result other than // NO_SUCH_OBJECT or INVALID_CREDENTIALS. If there aren't any such results, // then return the first failure. for (final ExtendedResult r : failures) { switch (r.getResultCode().intValue()) { case ResultCode.NO_SUCH_OBJECT_INT_VALUE: case ResultCode.INVALID_CREDENTIALS_INT_VALUE: break; default: return r; } } return failures.get(0); } /** * Retrieves the name of the attribute that should be used for username * lookups. * * @param parser The argument parser that should be used to make the * determination. * * @return The name of the attribute that should be used for username * lookups. */ private static String getUsernameAttribute(final ArgumentParser parser) { final Argument a = parser.getNamedArgument(ARG_NAME_USERNAME_ATTRIBUTE); if ((a != null) && (a instanceof StringArgument)) { return ((StringArgument) a).getValue(); } return DEFAULT_USERNAME_ATTRIBUTE; } /** * Extracts a password modify extended request from the provided information. * If the request is cached in the operation state (because it has already * been decoded for this operation) then use it. Otherwise, try to decode it * from the provided generic request. * * @param operationContext The operation context for the extended operation. * @param request The extended request to be processed. * * @return The extracted password modify extended request. * * @throws LDAPException If it is not possible to decode the provided * extended request as a password modify extended * request. */ private static PasswordModifyExtendedRequest getPasswordModifyRequest( final OperationContext operationContext, final ExtendedRequest request) throws LDAPException { // See if there is a cached version attached to the operation. final Object attachment = operationContext.getAttachment( ATTACHMENT_NAME_DECODED_REQUEST); if ((attachment != null) && (attachment instanceof PasswordModifyExtendedRequest)) { return (PasswordModifyExtendedRequest) attachment; } // Decode the provided extended request as a password modify request and // cache it in the operation state before returning it. final PasswordModifyExtendedRequest passwordModifyRequest = new PasswordModifyExtendedRequest(request.toLDAPSDKRequest()); operationContext.setAttachment(ATTACHMENT_NAME_DECODED_REQUEST, passwordModifyRequest); return passwordModifyRequest; } /** * Extracts the DN of the target user from the provided information. If it * has already been extracted, then we may find a cached version in the * operation state. * * @param operationContext The operation context for the extended operation. * @param request The extended request to be processed. * * @return The extracted target user DN, or {@code null} if the user is * identified by a username rather than a DN. * * @throws LDAPException If a problem is encountered while trying to decode * the target user DN. */ private static DN getTargetUserDN(final OperationContext operationContext, final PasswordModifyExtendedRequest request) throws LDAPException { // See if there is a cached version attached to the operation, then use it. final Object attachment = operationContext.getAttachment( ATTACHMENT_NAME_DECODED_USER_DN); if ((attachment != null) && (attachment instanceof DN)) { return (DN) attachment; } // Get the user identity from the request. If there isn't a user identity, // then use the authorization DN for the operation. final String userIdentity = request.getUserIdentity(); if (userIdentity == null) { final DN userDN = new DN(operationContext.getAuthorizationDN()); operationContext.setAttachment(ATTACHMENT_NAME_DECODED_USER_DN, userDN); return userDN; } // If the user identity starts with "dn:", then try to decode the rest of // the string as a DN. final String lowerUserIdentity = StaticUtils.toLowerCase(userIdentity); if (lowerUserIdentity.startsWith("dn:")) { final DN userDN = new DN(userIdentity.substring(3)); operationContext.setAttachment(ATTACHMENT_NAME_DECODED_USER_DN, userDN); return userDN; } // If the user identity starts with "u:", then the user is identified by // username rather than DN. if (lowerUserIdentity.startsWith("u:")) { final String username = userIdentity.substring(2); operationContext.setAttachment(ATTACHMENT_NAME_DECODED_USERNAME, username); return null; } // Try to decode the entire user identity string as a DN. final DN userDN = new DN(userIdentity); operationContext.setAttachment(ATTACHMENT_NAME_DECODED_USER_DN, userDN); return userDN; } /** * Extracts the username of the target user from the provided information. If * it has already been extracted, then we may find a cached version in the * operation state. * * @param operationContext The operation context for the extended operation. * @param request The extended request to be processed. * * @return The extracted target username, or {@code null} if the user is * identified by a DN (or defaults to the user's authorization DN) * rather than a username. */ private static String getTargetUsername( final OperationContext operationContext, final PasswordModifyExtendedRequest request) { // See if there is a cached version attached to the operation, then use it. final Object attachment = operationContext.getAttachment( ATTACHMENT_NAME_DECODED_USERNAME); if ((attachment != null) && (attachment instanceof String)) { return (String) attachment; } // Get the user identity from the request. If there isn't a user identity, // then the request doesn't target a user by username. final String userIdentity = request.getUserIdentity(); if (userIdentity == null) { return null; } // If the user identity starts with "u:", then the user is identified by // username rather than DN. final String lowerUserIdentity = StaticUtils.toLowerCase(userIdentity); if (lowerUserIdentity.startsWith("u:")) { final String username = userIdentity.substring(2); operationContext.setAttachment(ATTACHMENT_NAME_DECODED_USERNAME, username); return username; } // The request doesn't target a user by username. return null; } /** * {@inheritDoc} */ @Override() public Map<List<String>,String> getExamplesArgumentSets() { final LinkedHashMap<List<String>,String> examples = new LinkedHashMap<List<String>,String>(1); examples.put( Arrays.asList( ARG_NAME_USERNAME_ATTRIBUTE + "=mail"), "Allow the password modify extended operation to be proxied to " + "backend servers, using the mail attribute to perform username " + "lookups."); return Collections.unmodifiableMap(examples); } }