/*
* 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
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at
* trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable,
* add the following below this CDDL HEADER, with the fields enclosed
* by brackets "[]" replaced with your own identifying information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Copyright 2011-2013 ForgeRock AS.
*/
/**
* LDAP pass through authentication policy implementation.
*/
public final class LDAPPassThroughAuthenticationPolicyFactory implements
{
// TODO: handle password policy response controls? AD?
// TODO: custom aliveness pings
// TODO: improve debug logging and error messages.
/**
* A simplistic load-balancer connection factory implementation using
* approximately round-robin balancing.
*/
{
/**
* A connection which automatically retries operations on other servers.
*/
{
private final int startIndex;
private int nextIndex;
throws DirectoryException
{
do
{
if (factory.isAvailable)
{
try
{
return;
}
catch (final DirectoryException e)
{
// Ignore this error and try the next factory.
if (debugEnabled())
{
}
lastException = e;
}
}
else
{
}
}
while (nextIndex != startIndex);
// All the factories have been tried so give up and throw the exception.
throw lastException;
}
/**
* {@inheritDoc}
*/
public void close()
{
connection.close();
}
/**
* {@inheritDoc}
*/
{
for (;;)
{
try
{
}
catch (final DirectoryException e)
{
if (debugEnabled())
{
}
}
}
}
/**
* {@inheritDoc}
*/
{
for (;;)
{
try
{
return;
}
catch (final DirectoryException e)
{
if (debugEnabled())
{
}
}
}
}
throws DirectoryException
{
// If the error does not indicate that the connection has failed, then
// pass this back to the caller.
if (!isServiceError(e.getResultCode()))
{
throw e;
}
// The associated server is unavailable, so close the connection and
// try the next connection factory.
connection.close();
factory.lastException = e;
while (nextIndex != startIndex)
{
if (factory.isAvailable)
{
try
{
return;
}
catch (final DirectoryException de)
{
// Ignore this error and try the next factory.
if (debugEnabled())
{
}
}
}
}
// All the factories have been tried so give up and throw the exception.
throw e;
}
private void incrementNextIndex()
{
// Try the next index.
{
nextIndex = 0;
}
}
}
/**
* avoid unnecessary connection attempts when it is known to be offline.
*/
{
// isAvailable acts as memory barrier for lastException.
private volatile boolean isAvailable = true;
{
}
/**
* {@inheritDoc}
*/
public void close()
{
}
/**
* {@inheritDoc}
*/
{
try
{
isAvailable = true;
return connection;
}
catch (final DirectoryException e)
{
if (debugEnabled())
{
}
lastException = e;
isAvailable = false; // publishes lastException
throw e;
}
}
}
private final int maxIndex;
/**
* Creates a new abstract load-balancer.
*
* @param factories
* The list of underlying connection factories.
* @param scheduler
* The monitoring scheduler.
*/
{
for (int i = 0; i < maxIndex; i++)
{
}
}
/**
* Close underlying connection pools.
*/
public final void close()
{
monitorFuture.cancel(true);
{
}
}
/**
* {@inheritDoc}
*/
{
final int startIndex = getStartIndex();
return new FailoverConnection(startIndex);
}
/**
* Try to connect to any offline connection factories.
*/
public void run()
{
{
if (!factory.isAvailable)
{
try
{
}
catch (final DirectoryException e)
{
if (debugEnabled())
{
}
}
}
}
}
/**
* Return the start which should be used for the next connection attempt.
*
* @return The start which should be used for the next connection attempt.
*/
abstract int getStartIndex();
}
/**
* A factory which returns pre-authenticated connections for searches.
* <p>
* Package private for testing.
*/
static final class AuthenticatedConnectionFactory implements
{
/**
* Creates a new authenticated connection factory which will bind on
* connect.
*
* @param factory
* The underlying connection factory whose connections are to be
* authenticated.
* @param username
* The username taken from the configuration.
* @param password
* The password taken from the configuration.
*/
{
}
/**
* {@inheritDoc}
*/
public void close()
{
}
/**
* {@inheritDoc}
*/
{
{
try
{
}
catch (final DirectoryException e)
{
connection.close();
throw e;
}
}
return connection;
}
}
/**
* An LDAP connection which will be used in order to search for or
* authenticate users.
*/
{
/**
* Closes this connection.
*/
void close();
/**
* Returns the name of the user whose entry matches the provided search
* criteria. This will return CLIENT_SIDE_NO_RESULTS_RETURNED/NO_SUCH_OBJECT
* if no search results were returned, or CLIENT_SIDE_MORE_RESULTS_TO_RETURN
* if too many results were returned.
*
* @param baseDN
* The search base DN.
* @param scope
* The search scope.
* @param filter
* The search filter.
* @return The name of the user whose entry matches the provided search
* criteria.
* @throws DirectoryException
* If the search returned no entries, more than one entry, or if
* the search failed unexpectedly.
*/
throws DirectoryException;
/**
* Performs a simple bind for the user.
*
* @param username
* The user name (usually a bind DN).
* @param password
* The user's password.
* @throws DirectoryException
* If the credentials were invalid, or the authentication failed
* unexpectedly.
*/
throws DirectoryException;
}
/**
* An interface for obtaining connections: users of this interface will obtain
* a connection, perform a single operation (search or bind), and then close
* it.
*/
{
/**
* {@inheritDoc}
* <p>
* Must never throw an exception.
*/
void close();
/**
* Returns a connection which can be used in order to search for or
* authenticate users.
*
* @return The connection.
* @throws DirectoryException
* If an unexpected error occurred while attempting to obtain a
* connection.
*/
}
/**
* PTA connection pool.
* <p>
* Package private for testing.
*/
{
/**
* Pooled connection's intercept close and release connection back to the
* pool.
*/
{
private boolean connectionIsClosed = false;
{
this.connection = connection;
}
/**
* {@inheritDoc}
*/
public void close()
{
if (!connectionIsClosed)
{
connectionIsClosed = true;
// Guarded by PolicyImpl
if (poolIsClosed)
{
connection.close();
}
else
{
}
connection = null;
}
}
/**
* {@inheritDoc}
*/
{
try
{
}
catch (final DirectoryException e1)
{
// Fail immediately if the result indicates that the operation failed
// for a reason other than connection/server failure.
// The connection has failed, so retry the operation using the new
// connection.
try
{
}
catch (final DirectoryException e2)
{
// If the connection has failed again then give up: don't put the
// connection back in the pool.
throw e2;
}
}
}
/**
* {@inheritDoc}
*/
{
try
{
}
catch (final DirectoryException e1)
{
// Fail immediately if the result indicates that the operation failed
// for a reason other than connection/server failure.
// The connection has failed, so retry the operation using the new
// connection.
try
{
}
catch (final DirectoryException e2)
{
// If the connection has failed again then give up: don't put the
// connection back in the pool.
throw e2;
}
}
}
throws DirectoryException
{
if (isServiceError(e.getResultCode()))
{
connectionIsClosed = true;
connection.close();
connection = null;
}
}
throws DirectoryException
{
if (!isServiceError(e.getResultCode()))
{
throw e;
}
// The connection has failed (e.g. idle timeout), so repeat the
// request on a new connection.
connection.close();
try
{
}
catch (final DirectoryException e2)
{
// Give up - the server is unreachable.
connectionIsClosed = true;
connection = null;
throw e2;
}
}
}
// Guarded by PolicyImpl.lock.
private boolean poolIsClosed = false;
new ConcurrentLinkedQueue<Connection>();
/**
* Creates a new connection pool for the provided factory.
*
* @param factory
* The underlying connection factory whose connections are to be
* pooled.
*/
{
}
/**
* Release all connections: do we want to block?
*/
public void close()
{
// No need for synchronization as this can only be called with the
// policy's exclusive lock.
poolIsClosed = true;
{
connection.close();
}
// Since we have the exclusive lock, there should be no more connections
// in use.
{
throw new IllegalStateException(
"Pool has remaining connections open after close");
}
}
/**
* {@inheritDoc}
*/
{
// This should only be called with the policy's shared lock.
if (poolIsClosed)
{
throw new IllegalStateException("pool is closed");
}
// There is either a pooled connection or we are allowed to create
// one.
if (connection == null)
{
try
{
}
catch (final DirectoryException e)
{
throw e;
}
}
return new PooledConnection(connection);
}
}
/**
* A simplistic two-way fail-over connection factory implementation.
* <p>
* Package private for testing.
*/
{
/**
* Creates a new fail-over connection factory which will always try the
* primary connection factory first, before trying the second.
*
* @param primary
* The primary connection factory.
* @param secondary
* The secondary connection factory.
* @param scheduler
* The monitoring scheduler.
*/
final ConnectionFactory secondary,
{
}
/**
* {@inheritDoc}
*/
int getStartIndex()
{
// Always start with the primaries.
return 0;
}
}
/**
* The PTA design guarantees that connections are only used by a single thread
* at a time, so we do not need to perform any synchronization.
* <p>
* Package private for testing.
*/
{
/**
* LDAP connection implementation.
*/
{
private boolean isClosed = false;
{
this.plainSocket = plainSocket;
this.ldapSocket = ldapSocket;
}
/**
* {@inheritDoc}
*/
public void close()
{
/*
* This method is intentionally a bit "belt and braces" because we have
* seen far too many subtle resource leaks due to bugs within JDK,
* especially when used in conjunction with SSL (e.g.
*/
if (isClosed)
{
return;
}
isClosed = true;
// Send an unbind request.
new UnbindRequestProtocolOp());
try
{
}
catch (final IOException e)
{
if (debugEnabled())
{
}
}
// Close all IO resources.
try
{
ldapSocket.close();
}
catch (final IOException e)
{
if (debugEnabled())
{
}
}
try
{
plainSocket.close();
}
catch (final IOException e)
{
if (debugEnabled())
{
}
}
}
/**
* {@inheritDoc}
*/
{
// Create the search request and send it to the server.
final SearchRequestProtocolOp searchRequest =
// Read the responses from the server. We cannot fail-fast since this
// could leave unread search response messages.
byte opType;
int resultCount = 0;
do
{
switch (opType)
{
{
}
resultCount++;
break;
// The reference does not necessarily mean that there would have
// been any matching results, so lets ignore it.
break;
.getResultCode());
switch (resultCode)
{
case SUCCESS:
// The search succeeded. Drop out of the loop and check that we
// got a matching entry.
break;
case SIZE_LIMIT_EXCEEDED:
// Multiple matching candidates.
throw new DirectoryException(
default:
// The search failed for some reason.
throw new DirectoryException(resultCode,
}
break;
default:
// Check for disconnect notifications.
break;
}
}
while (opType != OP_TYPE_SEARCH_RESULT_DONE);
if (resultCount > 1)
{
// Multiple matching candidates.
throw new DirectoryException(
}
{
// No matching entries found.
throw new DirectoryException(
}
return username;
}
/**
* {@inheritDoc}
*/
{
// Create the bind request and send it to the server.
// Read the response from the server.
switch (responseMessage.getProtocolOpType())
{
case OP_TYPE_BIND_RESPONSE:
.getResultCode());
{
// FIXME: need to look for things like password expiration
// warning, reset notice, etc.
return;
}
else
{
// The bind failed for some reason.
throw new DirectoryException(resultCode,
}
default:
// Check for disconnect notifications.
break;
}
}
/**
* {@inheritDoc}
*/
protected void finalize()
{
close();
}
throws DirectoryException
{
{
if ((responseOID != null)
{
.getResultCode());
/*
* Since the connection has been disconnected we want to ensure that
* upper layers treat all disconnect notifications as fatal and
* close the connection. Therefore we map the result code to a fatal
* error code if needed. A good example of a non-fatal error code
* being returned is INVALID_CREDENTIALS which is used to indicate
* that the currently bound user has had their entry removed. We
* definitely don't want to pass this straight back to the caller
* since it will be misinterpreted as an authentication failure if
* the operation being performed is a bind.
*/
throw new DirectoryException(mappedResultCode,
}
}
// Unexpected response type.
}
// Reads a response message and adapts errors to directory exceptions.
{
final LDAPMessage responseMessage;
try
{
}
catch (final ASN1Exception e)
{
// ASN1 layer hides all underlying IO exceptions.
if (e.getCause() instanceof SocketTimeoutException)
{
}
else if (e.getCause() instanceof IOException)
{
}
else
{
}
}
catch (final LDAPException e)
{
}
catch (final SocketTimeoutException e)
{
}
catch (final IOException e)
{
}
if (responseMessage == null)
{
}
return responseMessage;
}
// Sends a request message and adapts errors to directory exceptions.
throws DirectoryException
{
request);
try
{
}
catch (final IOException e)
{
}
}
}
private final int port;
private final int timeoutMS;
/**
* LDAP connection factory implementation is package private so that it can
* be tested.
*
* @param host
* The server host name.
* @param port
* The server port.
* @param cfg
* The configuration (for SSL).
*/
{
// Normalize the timeoutMS to an integer (admin framework ensures that the
// value is non-negative).
}
/**
* {@inheritDoc}
*/
public void close()
{
// Nothing to do.
}
/**
* {@inheritDoc}
*/
{
try
{
// Create the remote ldapSocket address.
port);
// Create the ldapSocket and connect to the remote server.
try
{
// Set ldapSocket cfg before connecting.
// Connect the ldapSocket.
{
// Obtain the optional configured trust manager which will be used
// in order to determine the trust of the remote LDAP server.
if (trustManagerDN != null)
{
final TrustManagerProvider<?> trustManagerProvider =
if (trustManagerProvider != null)
{
}
}
// Create the SSL context and initialize it.
// Create the SSL socket.
.getSocketFactory();
sslSocket.setUseClientMode(true);
{
new String[0]));
}
{
new String[0]));
}
// Force TLS negotiation.
}
else
{
}
writer);
return ldapConnection;
}
finally
{
if (ldapConnection == null)
{
// Connection creation failed for some reason, so clean up IO
// resources.
{
}
{
}
if (ldapSocket != null)
{
try
{
ldapSocket.close();
}
catch (final IOException ignored)
{
// Ignore.
}
}
if (ldapSocket != plainSocket)
{
try
{
plainSocket.close();
}
catch (final IOException ignored)
{
// Ignore.
}
}
}
}
}
catch (final UnknownHostException e)
{
if (debugEnabled())
{
}
}
catch (final ConnectException e)
{
if (debugEnabled())
{
}
}
catch (final SocketTimeoutException e)
{
if (debugEnabled())
{
}
}
catch (final SSLException e)
{
if (debugEnabled())
{
}
}
catch (final Exception e)
{
if (debugEnabled())
{
}
}
}
}
/**
* An interface for obtaining a connection factory for LDAP connections to a
* named LDAP server and the monitoring scheduler.
*/
static interface Provider
{
/**
* Returns a connection factory which can be used for obtaining connections
* to the specified LDAP server.
*
* @param host
* The LDAP server host name.
* @param port
* The LDAP server port.
* @param cfg
* The LDAP connection configuration.
* @return A connection factory which can be used for obtaining connections
* to the specified LDAP server.
*/
/**
* Returns the scheduler which should be used to periodically ping
* connection factories to determine when they are online.
*
* @return The scheduler which should be used to periodically ping
* connection factories to determine when they are online.
*/
/**
* Returns the current time in order to perform cached password expiration
* checks. The returned string will be formatted as a a generalized time
* string
*
* @return The current time.
*/
/**
* Returns the current time in order to perform cached password expiration
* checks.
*
* @return The current time in MS.
*/
long getCurrentTimeMS();
}
/**
* A simplistic load-balancer connection factory implementation using
* approximately round-robin balancing.
*/
{
private final int maxIndex;
/**
* Creates a new load-balancer which will distribute connection requests
* across a set of underlying connection factories.
*
* @param factories
* The list of underlying connection factories.
* @param scheduler
* The monitoring scheduler.
*/
{
}
/**
* {@inheritDoc}
*/
int getStartIndex()
{
// A round robin pool of one connection factories is unlikely in
// practice and requires special treatment.
if (maxIndex == 1)
{
return 0;
}
// Determine the next factory to use: avoid blocking algorithm.
int oldNextIndex;
int newNextIndex;
do
{
if (newNextIndex == maxIndex)
{
newNextIndex = 0;
}
}
// There's a potential, but benign, race condition here: other threads
// could jump in and rotate through the list before we return the
// connection factory.
return oldNextIndex;
}
}
/**
* LDAP PTA policy implementation.
*/
{
/**
* LDAP PTA policy state implementation.
*/
{
{
super(userEntry);
}
/**
* {@inheritDoc}
*/
{
sharedLock.lock();
try
{
{
// Update the user's entry to contain the cached password and
// time stamp.
provider.getCurrentTime()));
{
// The modification failed for some reason. This should not
// prevent the bind from succeeded since we are only updating
// cache data. However, the performance of the server may be
// impacted, so log a debug warning message.
if (debugEnabled())
{
"An error occurred while trying to update the LDAP PTA "
.getErrorMessage()));
}
}
}
}
finally
{
sharedLock.unlock();
}
}
/**
* {@inheritDoc}
*/
{
return PolicyImpl.this;
}
/**
* {@inheritDoc}
*/
throws DirectoryException
{
sharedLock.lock();
try
{
// First check the cached password if enabled and available.
{
return true;
}
// The cache lookup failed, so perform full PTA.
switch (cfg.getMappingPolicy())
{
case UNMAPPED:
// The bind DN is the name of the user's entry.
break;
case MAPPED_BIND:
// The bind DN is contained in an attribute in the user's entry.
{
{
{
{
break mapBind;
}
}
}
}
{
/*
* The mapping attribute(s) is not present in the entry. This
* could be a configuration error, but it could also be because
* someone is attempting to authenticate using a bind DN which
* references a non-user entry.
*/
}
break;
case MAPPED_SEARCH:
// A search against the remote directory is required in order to
// determine the bind DN.
// Construct the search filter.
new LinkedList<SearchFilter>();
{
{
{
{
value));
}
}
}
}
if (filterComponents.isEmpty())
{
/*
* The mapping attribute(s) is not present in the entry. This
* could be a configuration error, but it could also be because
* someone is attempting to authenticate using a bind DN which
* references a non-user entry.
*/
}
final SearchFilter filter;
{
}
else
{
}
// Now search the configured base DNs, stopping at the first
// success.
{
try
{
filter);
}
catch (final DirectoryException e)
{
switch (e.getResultCode())
{
case NO_SUCH_OBJECT:
// Ignore and try next base DN.
break;
// More than one matching entry was returned.
default:
// We don't want to propagate this internal error to the
// client. We should log it and map it to a more appropriate
// error.
}
}
finally
{
if (connection != null)
{
connection.close();
}
}
}
{
/*
* No matching entries were found in the remote directory.
*/
}
break;
}
// Now perform the bind.
try
{
// The password matched, so cache it, it will be stored in the
// user's entry when the state is finalized and only if caching is
// enabled.
return true;
}
catch (final DirectoryException e)
{
switch (e.getResultCode())
{
case NO_SUCH_OBJECT:
case INVALID_CREDENTIALS:
return false;
default:
// We don't want to propagate this internal error to the
// client. We should log it and map it to a more appropriate
// error.
}
}
finally
{
if (connection != null)
{
connection.close();
}
}
}
finally
{
sharedLock.unlock();
}
}
{
if (!cfg.isUsePasswordCaching())
{
return false;
}
// First determine if the cached password time is present and valid.
boolean foundValidCachedPasswordTime = false;
{
{
{
// Ignore any attributes with options.
if (!attribute.hasOptions())
{
{
try
{
long expiryTime = cachedPasswordTime
}
catch (DirectoryException e)
{
// Fall-through and give up immediately.
if (debugEnabled())
{
}
}
break foundCachedPasswordTime;
}
}
}
}
}
{
// The cached password time was not found or it has expired, so give
// up immediately.
return false;
}
// Next determine if there is a cached password.
{
{
{
// Ignore any attributes with options.
if (!attribute.hasOptions())
{
{
break foundCachedPassword;
}
}
}
}
}
if (cachedPassword == null)
{
// The cached password was not found, so give up immediately.
return false;
}
// Decode the password and match it according to its storage scheme.
try
{
{
}
}
catch (DirectoryException e)
{
// Unable to decode the cached password, so give up.
if (debugEnabled())
{
}
}
return false;
}
}
// Guards against configuration changes.
// Current configuration.
private PolicyImpl(
{
}
/**
* {@inheritDoc}
*/
{
try
{
}
finally
{
}
}
/**
* {@inheritDoc}
*/
{
// The current time is not needed for LDAP PTA.
}
/**
* {@inheritDoc}
*/
public void finalizeAuthenticationPolicy()
{
try
{
}
finally
{
}
}
/**
* {@inheritDoc}
*/
{
}
/**
* {@inheritDoc}
*/
public boolean isConfigurationChangeAcceptable(
{
return LDAPPassThroughAuthenticationPolicyFactory.this
}
private void closeConnections()
{
try
{
if (searchFactory != null)
{
}
if (bindFactory != null)
{
bindFactory.close();
bindFactory = null;
}
}
finally
{
}
}
private void initializeConfiguration(
{
// First obtain the mapped search password if needed, ignoring any errors
// since these should have already been detected during configuration
// validation.
final String mappedSearchPassword;
{
new LinkedList<Message>());
}
else
{
}
// Use two pools per server: one for authentication (bind) and one for
// searches. Even if the searches are performed anonymously we cannot use
// the same pool, otherwise they will be performed as the most recently
// authenticated user.
// Create load-balancers for primary servers.
int index = 0;
{
}
// Create load-balancers for secondary servers.
{
}
else
{
index = 0;
{
}
}
if (cfg.isUsePasswordCaching())
{
}
}
{
// Validation already performed by admin framework.
}
}
// Debug tracer for this class.
/**
* Attribute list for searches requesting no attributes.
*/
1);
static
{
}
// The provider which should be used by policies to create LDAP connections.
/**
* The default LDAP connection factory provider.
*/
{
// Global scheduler used for periodically monitoring connection factories in
// order to detect when they are online.
{
{
final Thread t = new DirectoryThread(r,
"LDAP PTA connection monitor thread");
t.setDaemon(true);
return t;
}
});
{
}
{
return scheduler;
}
public String getCurrentTime()
{
return TimeThread.getGMTTime();
}
public long getCurrentTimeMS()
{
return TimeThread.getTime();
}
};
/**
* Determines whether or no a result code is expected to trigger the
* associated connection to be closed immediately.
*
* @param resultCode
* The result code.
* @return {@code true} if the result code is expected to trigger the
* associated connection to be closed immediately.
*/
{
switch (resultCode)
{
case OPERATIONS_ERROR:
case PROTOCOL_ERROR:
case TIME_LIMIT_EXCEEDED:
case ADMIN_LIMIT_EXCEEDED:
case BUSY:
case UNAVAILABLE:
case UNWILLING_TO_PERFORM:
case LOOP_DETECT:
case OTHER:
case CLIENT_SIDE_LOCAL_ERROR:
case CLIENT_SIDE_SERVER_DOWN:
case CLIENT_SIDE_TIMEOUT:
return true;
default:
return false;
}
}
//Get the search bind password performing mapped searches.
//
// We will offer several places to look for the password, and we will
// do so in the following order:
//
// - In a specified Java property
// - In a specified environment variable
// - In a specified file on the server filesystem.
// - As the value of a configuration attribute.
//
// In any case, the password must be in the clear.
{
{
{
}
}
{
{
}
}
{
if (!passwordFile.exists())
{
}
else
{
try
{
{
}
}
catch (IOException e)
{
getExceptionMessage(e)));
}
finally
{
try
{
}
catch (Exception e)
{
// Ignored.
}
}
}
}
{
}
else
{
// Password wasn't defined anywhere.
}
return password;
}
private static boolean isServerAddressValid(
{
{
if (unacceptableReasons != null)
{
}
return false;
}
return true;
}
{
switch (attributes.size())
{
case 0:
return "";
case 1:
default:
while (i.hasNext())
{
}
}
}
/**
* Public default constructor used by the admin framework. This will use the
* default LDAP connection factory provider.
*/
{
this(DEFAULT_PROVIDER);
}
/**
* Package private constructor allowing unit tests to provide mock connection
* implementations.
*
* @param provider
* The LDAP connection factory provider implementation which LDAP PTA
* authentication policies will use.
*/
{
}
/**
* {@inheritDoc}
*/
{
return policy;
}
/**
* {@inheritDoc}
*/
public boolean isConfigurationAcceptable(
{
// Check that the port numbers are valid. We won't actually try and connect
// to the server since they may not be available (hence we have fail-over
// capabilities).
boolean configurationIsAcceptable = true;
{
}
{
}
// Ensure that the search bind password is defined somewhere.
{
{
configurationIsAcceptable = false;
}
}
return configurationIsAcceptable;
}
}