/*
* 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 2014-2016 UnboundID Corp.
*/
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);
}
}
|