/*
|
* 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
|
* trunk/opends/resource/legal-notices/OpenDS.LICENSE
|
* or https://OpenDS.dev.java.net/OpenDS.LICENSE.
|
* 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 ForgeRock AS.
|
*/
|
|
package org.opends.server.extensions;
|
|
|
|
import static org.opends.messages.ExtensionMessages.*;
|
import static org.opends.server.loggers.debug.DebugLogger.debugEnabled;
|
import static org.opends.server.protocols.ldap.LDAPConstants.*;
|
|
import java.io.Closeable;
|
import java.io.IOException;
|
import java.net.*;
|
import java.util.*;
|
import java.util.concurrent.*;
|
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
|
import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
|
|
import javax.net.ssl.*;
|
|
import org.opends.messages.Message;
|
import org.opends.server.admin.server.ConfigurationChangeListener;
|
import org.opends.server.admin.std.server.*;
|
import org.opends.server.api.*;
|
import org.opends.server.config.ConfigException;
|
import org.opends.server.core.DirectoryServer;
|
import org.opends.server.loggers.debug.DebugLogger;
|
import org.opends.server.loggers.debug.DebugTracer;
|
import org.opends.server.protocols.asn1.ASN1Exception;
|
import org.opends.server.protocols.ldap.*;
|
import org.opends.server.tools.LDAPReader;
|
import org.opends.server.tools.LDAPWriter;
|
import org.opends.server.types.*;
|
|
|
|
/**
|
* LDAP pass through authentication policy implementation.
|
*/
|
public final class LDAPPassThroughAuthenticationPolicyFactory implements
|
AuthenticationPolicyFactory<LDAPPassThroughAuthenticationPolicyCfg>
|
{
|
|
// TODO: handle password policy response controls? AD?
|
// TODO: provide alternative cfg for search password.
|
// TODO: custom aliveness pings
|
// TODO: manage account lockout
|
// TODO: cache password
|
|
/**
|
* A simplistic load-balancer connection factory implementation using
|
* approximately round-robin balancing.
|
*/
|
static abstract class AbstractLoadBalancer implements ConnectionFactory,
|
Runnable
|
{
|
/**
|
* A connection which automatically retries operations on other servers.
|
*/
|
private final class FailoverConnection implements Connection
|
{
|
private Connection connection;
|
private MonitoredConnectionFactory factory;
|
private final int startIndex;
|
private int nextIndex;
|
|
|
|
private FailoverConnection(final int startIndex)
|
throws DirectoryException
|
{
|
this.startIndex = nextIndex = startIndex;
|
|
DirectoryException lastException = null;
|
do
|
{
|
factory = factories[nextIndex];
|
if (factory.isAvailable())
|
{
|
try
|
{
|
if (factory.isAvailable)
|
{
|
connection = factory.getConnection();
|
incrementNextIndex();
|
return;
|
}
|
}
|
catch (final DirectoryException e)
|
{
|
// Ignore this error and try the next factory.
|
if (debugEnabled())
|
{
|
TRACER.debugCaught(DebugLogLevel.ERROR, e);
|
}
|
lastException = e;
|
}
|
}
|
incrementNextIndex();
|
}
|
while (nextIndex != startIndex);
|
|
// All the factories have been tried so give up and throw the exception.
|
throw lastException;
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public void close()
|
{
|
connection.close();
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public ByteString search(final DN baseDN, final SearchScope scope,
|
final SearchFilter filter) throws DirectoryException
|
{
|
for (;;)
|
{
|
try
|
{
|
return connection.search(baseDN, scope, filter);
|
}
|
catch (final DirectoryException e)
|
{
|
if (debugEnabled())
|
{
|
TRACER.debugCaught(DebugLogLevel.ERROR, e);
|
}
|
handleDirectoryException(e);
|
}
|
}
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public void simpleBind(final ByteString username,
|
final ByteString password) throws DirectoryException
|
{
|
for (;;)
|
{
|
try
|
{
|
connection.simpleBind(username, password);
|
return;
|
}
|
catch (final DirectoryException e)
|
{
|
if (debugEnabled())
|
{
|
TRACER.debugCaught(DebugLogLevel.ERROR, e);
|
}
|
handleDirectoryException(e);
|
}
|
}
|
}
|
|
|
|
private void handleDirectoryException(final DirectoryException e)
|
throws DirectoryException
|
{
|
// If the error does not indicate that the connection has failed, then
|
// pass this back to the caller.
|
if (!isFatalResultCode(e.getResultCode()))
|
{
|
throw e;
|
}
|
|
// The associated server is unavailable, so close the connection and
|
// try the next connection factory.
|
connection.close();
|
factory.isAvailable = false;
|
|
while (nextIndex != startIndex)
|
{
|
factory = factories[nextIndex];
|
if (factory.isAvailable())
|
{
|
try
|
{
|
if (factory.isAvailable)
|
{
|
connection = factory.getConnection();
|
incrementNextIndex();
|
return;
|
}
|
}
|
catch (final DirectoryException de)
|
{
|
// Ignore this error and try the next factory.
|
if (debugEnabled())
|
{
|
TRACER.debugCaught(DebugLogLevel.ERROR, de);
|
}
|
}
|
}
|
incrementNextIndex();
|
}
|
|
// All the factories have been tried so give up and throw the exception.
|
throw e;
|
}
|
|
|
|
private void incrementNextIndex()
|
{
|
// Try the next index.
|
if (++nextIndex == maxIndex)
|
{
|
nextIndex = 0;
|
}
|
}
|
|
}
|
|
|
|
/**
|
* A connection factory which caches its online/offline state in order to
|
* avoid unnecessary connection attempts when it is known to be offline.
|
*/
|
private final class MonitoredConnectionFactory implements ConnectionFactory
|
{
|
private final ConnectionFactory factory;
|
private volatile boolean isAvailable = true;
|
|
|
|
private MonitoredConnectionFactory(final ConnectionFactory factory)
|
{
|
this.factory = factory;
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public void close()
|
{
|
factory.close();
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public Connection getConnection() throws DirectoryException
|
{
|
try
|
{
|
final Connection connection = factory.getConnection();
|
isAvailable = true;
|
return connection;
|
}
|
catch (final DirectoryException e)
|
{
|
if (debugEnabled())
|
{
|
TRACER.debugCaught(DebugLogLevel.ERROR, e);
|
}
|
isAvailable = false;
|
throw e;
|
}
|
}
|
|
|
|
private boolean isAvailable()
|
{
|
return isAvailable;
|
}
|
}
|
|
|
|
private final MonitoredConnectionFactory[] factories;
|
private final int maxIndex;
|
private final ScheduledFuture<?> monitorFuture;
|
|
|
|
/**
|
* Creates a new abstract load-balancer.
|
*
|
* @param factories
|
* The list of underlying connection factories.
|
* @param scheduler
|
* The monitoring scheduler.
|
*/
|
AbstractLoadBalancer(final ConnectionFactory[] factories,
|
final ScheduledExecutorService scheduler)
|
{
|
this.factories = new MonitoredConnectionFactory[factories.length];
|
this.maxIndex = factories.length;
|
|
for (int i = 0; i < maxIndex; i++)
|
{
|
this.factories[i] = new MonitoredConnectionFactory(factories[i]);
|
}
|
|
this.monitorFuture = scheduler.scheduleWithFixedDelay(this, 5, 5,
|
TimeUnit.SECONDS);
|
}
|
|
|
|
/**
|
* Close underlying connection pools.
|
*/
|
@Override
|
public final void close()
|
{
|
monitorFuture.cancel(true);
|
|
for (final ConnectionFactory factory : factories)
|
{
|
factory.close();
|
}
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public final Connection getConnection() throws DirectoryException
|
{
|
final int startIndex = getStartIndex();
|
return new FailoverConnection(startIndex);
|
}
|
|
|
|
/**
|
* Try to connect to any offline connection factories.
|
*/
|
@Override
|
public void run()
|
{
|
for (final MonitoredConnectionFactory factory : factories)
|
{
|
if (!factory.isAvailable())
|
{
|
try
|
{
|
factory.getConnection().close();
|
}
|
catch (final DirectoryException e)
|
{
|
if (debugEnabled())
|
{
|
TRACER.debugCaught(DebugLogLevel.ERROR, e);
|
}
|
}
|
}
|
}
|
}
|
|
|
|
/**
|
* 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
|
ConnectionFactory
|
{
|
|
private final ConnectionFactory factory;
|
private final DN username;
|
private final String password;
|
|
|
|
/**
|
* 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.
|
*/
|
AuthenticatedConnectionFactory(final ConnectionFactory factory,
|
final DN username, final String password)
|
{
|
this.factory = factory;
|
this.username = username;
|
this.password = password;
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public void close()
|
{
|
factory.close();
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public Connection getConnection() throws DirectoryException
|
{
|
final Connection connection = factory.getConnection();
|
if (username != null && !username.isNullDN() && password != null
|
&& password.length() > 0)
|
{
|
try
|
{
|
connection.simpleBind(ByteString.valueOf(username.toString()),
|
ByteString.valueOf(password));
|
}
|
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.
|
*/
|
static interface Connection extends Closeable
|
{
|
|
/**
|
* Closes this connection.
|
*/
|
@Override
|
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.
|
*/
|
ByteString search(DN baseDN, SearchScope scope, SearchFilter filter)
|
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.
|
*/
|
void simpleBind(ByteString username, ByteString password)
|
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.
|
*/
|
static interface ConnectionFactory extends Closeable
|
{
|
/**
|
* {@inheritDoc}
|
* <p>
|
* Must never throw an exception.
|
*/
|
@Override
|
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.
|
*/
|
Connection getConnection() throws DirectoryException;
|
}
|
|
|
|
/**
|
* PTA connection pool.
|
* <p>
|
* Package private for testing.
|
*/
|
static final class ConnectionPool implements ConnectionFactory
|
{
|
|
/**
|
* Pooled connection's intercept close and release connection back to the
|
* pool.
|
*/
|
private final class PooledConnection implements Connection
|
{
|
private final Connection connection;
|
private boolean connectionIsClosed = false;
|
|
|
|
private PooledConnection(final Connection connection)
|
{
|
this.connection = connection;
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public void close()
|
{
|
if (!connectionIsClosed)
|
{
|
connectionIsClosed = true;
|
|
// Guarded by PolicyImpl
|
if (poolIsClosed)
|
{
|
connection.close();
|
}
|
else
|
{
|
connectionPool.offer(connection);
|
}
|
availableConnections.release();
|
}
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public ByteString search(final DN baseDN, final SearchScope scope,
|
final SearchFilter filter) throws DirectoryException
|
{
|
try
|
{
|
return connection.search(baseDN, scope, filter);
|
}
|
catch (final DirectoryException e)
|
{
|
// Don't put the connection back in the pool if it has failed.
|
closeConnectionOnFatalError(e);
|
throw e;
|
}
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public void simpleBind(final ByteString username,
|
final ByteString password) throws DirectoryException
|
{
|
try
|
{
|
connection.simpleBind(username, password);
|
}
|
catch (final DirectoryException e)
|
{
|
// Don't put the connection back in the pool if it has failed.
|
closeConnectionOnFatalError(e);
|
throw e;
|
}
|
}
|
|
|
|
private void closeConnectionOnFatalError(final DirectoryException e)
|
{
|
if (isFatalResultCode(e.getResultCode()))
|
{
|
if (!connectionIsClosed)
|
{
|
connectionIsClosed = true;
|
connection.close();
|
availableConnections.release();
|
}
|
}
|
}
|
|
}
|
|
|
|
// Guarded by PolicyImpl.lock.
|
private boolean poolIsClosed = false;
|
|
private final ConnectionFactory factory;
|
private final int poolSize = Runtime.getRuntime().availableProcessors() * 2;
|
private final Semaphore availableConnections = new Semaphore(poolSize);
|
private final Queue<Connection> connectionPool =
|
new ConcurrentLinkedQueue<Connection>();
|
|
|
|
/**
|
* Creates a new connection pool for the provided factory.
|
*
|
* @param factory
|
* The underlying connection factory whose connections are to be
|
* pooled.
|
*/
|
ConnectionPool(final ConnectionFactory factory)
|
{
|
this.factory = factory;
|
}
|
|
|
|
/**
|
* Release all connections: do we want to block?
|
*/
|
@Override
|
public void close()
|
{
|
// No need for synchronization as this can only be called with the
|
// policy's exclusive lock.
|
poolIsClosed = true;
|
|
Connection connection;
|
while ((connection = connectionPool.poll()) != null)
|
{
|
connection.close();
|
}
|
|
factory.close();
|
|
// Since we have the exclusive lock, there should be no more connections
|
// in use.
|
if (availableConnections.availablePermits() != poolSize)
|
{
|
throw new IllegalStateException(
|
"Pool has remaining connections open after close");
|
}
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public Connection getConnection() throws DirectoryException
|
{
|
// This should only be called with the policy's shared lock.
|
if (poolIsClosed)
|
{
|
throw new IllegalStateException("pool is closed");
|
}
|
|
availableConnections.acquireUninterruptibly();
|
|
// There is either a pooled connection or we are allowed to create
|
// one.
|
Connection connection = connectionPool.poll();
|
if (connection == null)
|
{
|
try
|
{
|
connection = factory.getConnection();
|
}
|
catch (final DirectoryException e)
|
{
|
availableConnections.release();
|
throw e;
|
}
|
}
|
|
return new PooledConnection(connection);
|
}
|
}
|
|
|
|
/**
|
* A simplistic two-way fail-over connection factory implementation.
|
* <p>
|
* Package private for testing.
|
*/
|
static final class FailoverLoadBalancer extends AbstractLoadBalancer
|
{
|
|
/**
|
* 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.
|
*/
|
FailoverLoadBalancer(final ConnectionFactory primary,
|
final ConnectionFactory secondary,
|
final ScheduledExecutorService scheduler)
|
{
|
super(new ConnectionFactory[] { primary, secondary }, scheduler);
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
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.
|
*/
|
static final class LDAPConnectionFactory implements ConnectionFactory
|
{
|
/**
|
* LDAP connection implementation.
|
*/
|
private final class LDAPConnection implements Connection
|
{
|
private final Socket plainSocket;
|
private final Socket ldapSocket;
|
private final LDAPWriter writer;
|
private final LDAPReader reader;
|
private int nextMessageID = 1;
|
private boolean isClosed = false;
|
|
|
|
private LDAPConnection(final Socket plainSocket, final Socket ldapSocket,
|
final LDAPReader reader, final LDAPWriter writer)
|
{
|
this.plainSocket = plainSocket;
|
this.ldapSocket = ldapSocket;
|
this.reader = reader;
|
this.writer = writer;
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
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.
|
* http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=7025227).
|
*/
|
if (isClosed)
|
{
|
return;
|
}
|
isClosed = true;
|
|
// Send an unbind request.
|
final LDAPMessage message = new LDAPMessage(nextMessageID++,
|
new UnbindRequestProtocolOp());
|
try
|
{
|
writer.writeMessage(message);
|
}
|
catch (final IOException e)
|
{
|
if (debugEnabled())
|
{
|
TRACER.debugCaught(DebugLogLevel.ERROR, e);
|
}
|
}
|
|
// Close all IO resources.
|
writer.close();
|
reader.close();
|
|
try
|
{
|
ldapSocket.close();
|
}
|
catch (final IOException e)
|
{
|
if (debugEnabled())
|
{
|
TRACER.debugCaught(DebugLogLevel.ERROR, e);
|
}
|
}
|
|
try
|
{
|
plainSocket.close();
|
}
|
catch (final IOException e)
|
{
|
if (debugEnabled())
|
{
|
TRACER.debugCaught(DebugLogLevel.ERROR, e);
|
}
|
}
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public ByteString search(final DN baseDN, final SearchScope scope,
|
final SearchFilter filter) throws DirectoryException
|
{
|
// Create the search request and send it to the server.
|
final SearchRequestProtocolOp searchRequest =
|
new SearchRequestProtocolOp(
|
ByteString.valueOf(baseDN.toString()), scope,
|
DereferencePolicy.DEREF_ALWAYS, 1 /* size limit */,
|
(timeoutMS / 1000), true /* types only */,
|
RawFilter.create(filter), NO_ATTRIBUTES);
|
sendRequest(searchRequest);
|
|
// Read the responses from the server. We cannot fail-fast since this
|
// could leave unread search response messages.
|
byte opType;
|
ByteString username = null;
|
int resultCount = 0;
|
|
do
|
{
|
final LDAPMessage responseMessage = readResponse();
|
opType = responseMessage.getProtocolOpType();
|
|
switch (opType)
|
{
|
case OP_TYPE_SEARCH_RESULT_ENTRY:
|
final SearchResultEntryProtocolOp searchEntry = responseMessage
|
.getSearchResultEntryProtocolOp();
|
if (username == null)
|
{
|
username = ByteString.valueOf(searchEntry.getDN().toString());
|
}
|
resultCount++;
|
break;
|
|
case OP_TYPE_SEARCH_RESULT_REFERENCE:
|
// The reference does not necessarily mean that there would have
|
// been any matching results, so lets ignore it.
|
break;
|
|
case OP_TYPE_SEARCH_RESULT_DONE:
|
final SearchResultDoneProtocolOp searchResult = responseMessage
|
.getSearchResultDoneProtocolOp();
|
|
final ResultCode resultCode = ResultCode.valueOf(searchResult
|
.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(
|
ResultCode.CLIENT_SIDE_MORE_RESULTS_TO_RETURN,
|
ERR_LDAP_PTA_CONNECTION_SEARCH_SIZE_LIMIT.get(host, port,
|
String.valueOf(cfg.dn()), String.valueOf(baseDN),
|
String.valueOf(filter)));
|
|
default:
|
// The search failed for some reason.
|
throw new DirectoryException(resultCode,
|
ERR_LDAP_PTA_CONNECTION_SEARCH_FAILED.get(host, port,
|
String.valueOf(cfg.dn()), String.valueOf(baseDN),
|
String.valueOf(filter), resultCode.getIntValue(),
|
resultCode.getResultCodeName(),
|
searchResult.getErrorMessage()));
|
}
|
|
break;
|
|
default:
|
// Check for disconnect notifications.
|
handleUnexpectedResponse(responseMessage);
|
break;
|
}
|
}
|
while (opType != OP_TYPE_SEARCH_RESULT_DONE);
|
|
if (resultCount > 1)
|
{
|
// Multiple matching candidates.
|
throw new DirectoryException(
|
ResultCode.CLIENT_SIDE_MORE_RESULTS_TO_RETURN,
|
ERR_LDAP_PTA_CONNECTION_SEARCH_SIZE_LIMIT.get(host, port,
|
String.valueOf(cfg.dn()), String.valueOf(baseDN),
|
String.valueOf(filter)));
|
}
|
|
if (username == null)
|
{
|
// No matching entries found.
|
throw new DirectoryException(
|
ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED,
|
ERR_LDAP_PTA_CONNECTION_SEARCH_NO_MATCHES.get(host, port,
|
String.valueOf(cfg.dn()), String.valueOf(baseDN),
|
String.valueOf(filter)));
|
}
|
|
return username;
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public void simpleBind(final ByteString username,
|
final ByteString password) throws DirectoryException
|
{
|
// Create the bind request and send it to the server.
|
final BindRequestProtocolOp bindRequest = new BindRequestProtocolOp(
|
username, 3, password);
|
sendRequest(bindRequest);
|
|
// Read the response from the server.
|
final LDAPMessage responseMessage = readResponse();
|
switch (responseMessage.getProtocolOpType())
|
{
|
case OP_TYPE_BIND_RESPONSE:
|
final BindResponseProtocolOp bindResponse = responseMessage
|
.getBindResponseProtocolOp();
|
|
final ResultCode resultCode = ResultCode.valueOf(bindResponse
|
.getResultCode());
|
if (resultCode == ResultCode.SUCCESS)
|
{
|
// 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,
|
ERR_LDAP_PTA_CONNECTION_BIND_FAILED.get(host, port,
|
String.valueOf(cfg.dn()), String.valueOf(username),
|
resultCode.getIntValue(), resultCode.getResultCodeName(),
|
bindResponse.getErrorMessage()));
|
}
|
|
default:
|
// Check for disconnect notifications.
|
handleUnexpectedResponse(responseMessage);
|
break;
|
}
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
protected void finalize()
|
{
|
close();
|
}
|
|
|
|
private void handleUnexpectedResponse(final LDAPMessage responseMessage)
|
throws DirectoryException
|
{
|
if (responseMessage.getProtocolOpType() == OP_TYPE_EXTENDED_RESPONSE)
|
{
|
final ExtendedResponseProtocolOp extendedResponse = responseMessage
|
.getExtendedResponseProtocolOp();
|
final String responseOID = extendedResponse.getOID();
|
|
if ((responseOID != null)
|
&& responseOID.equals(OID_NOTICE_OF_DISCONNECTION))
|
{
|
throw new DirectoryException(ResultCode.valueOf(extendedResponse
|
.getResultCode()), ERR_LDAP_PTA_CONNECTION_DISCONNECTING.get(
|
host, port, String.valueOf(cfg.dn()),
|
extendedResponse.getErrorMessage()));
|
}
|
}
|
|
// Unexpected response type.
|
throw new DirectoryException(ResultCode.CLIENT_SIDE_DECODING_ERROR,
|
ERR_LDAP_PTA_CONNECTION_WRONG_RESPONSE.get(host, port,
|
String.valueOf(cfg.dn()),
|
String.valueOf(responseMessage.getProtocolOp())));
|
}
|
|
|
|
// Reads a response message and adapts errors to directory exceptions.
|
private LDAPMessage readResponse() throws DirectoryException
|
{
|
final LDAPMessage responseMessage;
|
try
|
{
|
responseMessage = reader.readMessage();
|
}
|
catch (final ASN1Exception e)
|
{
|
// ASN1 layer hides all underlying IO exceptions.
|
if (e.getCause() instanceof SocketTimeoutException)
|
{
|
throw new DirectoryException(ResultCode.CLIENT_SIDE_TIMEOUT,
|
ERR_LDAP_PTA_CONNECTION_TIMEOUT.get(host, port,
|
String.valueOf(cfg.dn())), e);
|
}
|
else if (e.getCause() instanceof IOException)
|
{
|
throw new DirectoryException(ResultCode.CLIENT_SIDE_SERVER_DOWN,
|
ERR_LDAP_PTA_CONNECTION_OTHER_ERROR.get(host, port,
|
String.valueOf(cfg.dn()), e.getMessage()), e);
|
}
|
else
|
{
|
throw new DirectoryException(ResultCode.CLIENT_SIDE_DECODING_ERROR,
|
ERR_LDAP_PTA_CONNECTION_DECODE_ERROR.get(host, port,
|
String.valueOf(cfg.dn()), e.getMessage()), e);
|
}
|
}
|
catch (final LDAPException e)
|
{
|
throw new DirectoryException(ResultCode.CLIENT_SIDE_DECODING_ERROR,
|
ERR_LDAP_PTA_CONNECTION_DECODE_ERROR.get(host, port,
|
String.valueOf(cfg.dn()), e.getMessage()), e);
|
}
|
catch (final SocketTimeoutException e)
|
{
|
throw new DirectoryException(ResultCode.CLIENT_SIDE_TIMEOUT,
|
ERR_LDAP_PTA_CONNECTION_TIMEOUT.get(host, port,
|
String.valueOf(cfg.dn())), e);
|
}
|
catch (final IOException e)
|
{
|
throw new DirectoryException(ResultCode.CLIENT_SIDE_SERVER_DOWN,
|
ERR_LDAP_PTA_CONNECTION_OTHER_ERROR.get(host, port,
|
String.valueOf(cfg.dn()), e.getMessage()), e);
|
}
|
|
if (responseMessage == null)
|
{
|
throw new DirectoryException(ResultCode.CLIENT_SIDE_SERVER_DOWN,
|
ERR_LDAP_PTA_CONNECTION_CLOSED.get(host, port,
|
String.valueOf(cfg.dn())));
|
}
|
return responseMessage;
|
}
|
|
|
|
// Sends a request message and adapts errors to directory exceptions.
|
private void sendRequest(final ProtocolOp request)
|
throws DirectoryException
|
{
|
final LDAPMessage requestMessage = new LDAPMessage(nextMessageID++,
|
request);
|
try
|
{
|
writer.writeMessage(requestMessage);
|
}
|
catch (final IOException e)
|
{
|
throw new DirectoryException(ResultCode.CLIENT_SIDE_SERVER_DOWN,
|
ERR_LDAP_PTA_CONNECTION_OTHER_ERROR.get(host, port,
|
String.valueOf(cfg.dn()), e.getMessage()), e);
|
}
|
}
|
}
|
|
|
|
private final String host;
|
private final int port;
|
private final LDAPPassThroughAuthenticationPolicyCfg cfg;
|
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).
|
*/
|
LDAPConnectionFactory(final String host, final int port,
|
final LDAPPassThroughAuthenticationPolicyCfg cfg)
|
{
|
this.host = host;
|
this.port = port;
|
this.cfg = cfg;
|
|
// Normalize the timeoutMS to an integer (admin framework ensures that the
|
// value is non-negative).
|
this.timeoutMS = (int) Math.min(cfg.getConnectionTimeout(),
|
Integer.MAX_VALUE);
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public void close()
|
{
|
// Nothing to do.
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public Connection getConnection() throws DirectoryException
|
{
|
try
|
{
|
// Create the remote ldapSocket address.
|
final InetAddress address = InetAddress.getByName(host);
|
final InetSocketAddress socketAddress = new InetSocketAddress(address,
|
port);
|
|
// Create the ldapSocket and connect to the remote server.
|
final Socket plainSocket = new Socket();
|
Socket ldapSocket = null;
|
LDAPReader reader = null;
|
LDAPWriter writer = null;
|
LDAPConnection ldapConnection = null;
|
|
try
|
{
|
// Set ldapSocket cfg before connecting.
|
plainSocket.setTcpNoDelay(cfg.isUseTCPNoDelay());
|
plainSocket.setKeepAlive(cfg.isUseTCPKeepAlive());
|
plainSocket.setSoTimeout(timeoutMS);
|
|
// Connect the ldapSocket.
|
plainSocket.connect(socketAddress, timeoutMS);
|
|
if (cfg.isUseSSL())
|
{
|
// Obtain the optional configured trust manager which will be used
|
// in order to determine the trust of the remote LDAP server.
|
TrustManager[] tm = null;
|
final DN trustManagerDN = cfg.getTrustManagerProviderDN();
|
if (trustManagerDN != null)
|
{
|
final TrustManagerProvider<?> trustManagerProvider =
|
DirectoryServer.getTrustManagerProvider(trustManagerDN);
|
if (trustManagerProvider != null)
|
{
|
tm = trustManagerProvider.getTrustManagers();
|
}
|
}
|
|
// Create the SSL context and initialize it.
|
final SSLContext sslContext = SSLContext.getInstance("TLS");
|
sslContext.init(null /* key managers */, tm, null /* rng */);
|
|
// Create the SSL socket.
|
final SSLSocketFactory sslSocketFactory = sslContext
|
.getSocketFactory();
|
final SSLSocket sslSocket = (SSLSocket) sslSocketFactory
|
.createSocket(plainSocket, host, port, true);
|
ldapSocket = sslSocket;
|
|
sslSocket.setUseClientMode(true);
|
if (!cfg.getSSLProtocol().isEmpty())
|
{
|
sslSocket.setEnabledProtocols(cfg.getSSLProtocol().toArray(
|
new String[0]));
|
}
|
if (!cfg.getSSLCipherSuite().isEmpty())
|
{
|
sslSocket.setEnabledCipherSuites(cfg.getSSLCipherSuite().toArray(
|
new String[0]));
|
}
|
|
// Force TLS negotiation.
|
sslSocket.startHandshake();
|
}
|
else
|
{
|
ldapSocket = plainSocket;
|
}
|
|
reader = new LDAPReader(ldapSocket);
|
writer = new LDAPWriter(ldapSocket);
|
|
ldapConnection = new LDAPConnection(plainSocket, ldapSocket, reader,
|
writer);
|
|
return ldapConnection;
|
}
|
finally
|
{
|
if (ldapConnection == null)
|
{
|
// Connection creation failed for some reason, so clean up IO
|
// resources.
|
if (reader != null)
|
{
|
reader.close();
|
}
|
if (writer != null)
|
{
|
writer.close();
|
}
|
|
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())
|
{
|
TRACER.debugCaught(DebugLogLevel.ERROR, e);
|
}
|
throw new DirectoryException(ResultCode.CLIENT_SIDE_CONNECT_ERROR,
|
ERR_LDAP_PTA_CONNECT_UNKNOWN_HOST.get(host, port,
|
String.valueOf(cfg.dn()), host), e);
|
}
|
catch (final ConnectException e)
|
{
|
if (debugEnabled())
|
{
|
TRACER.debugCaught(DebugLogLevel.ERROR, e);
|
}
|
throw new DirectoryException(ResultCode.CLIENT_SIDE_CONNECT_ERROR,
|
ERR_LDAP_PTA_CONNECT_ERROR.get(host, port,
|
String.valueOf(cfg.dn()), port), e);
|
}
|
catch (final SocketTimeoutException e)
|
{
|
if (debugEnabled())
|
{
|
TRACER.debugCaught(DebugLogLevel.ERROR, e);
|
}
|
throw new DirectoryException(ResultCode.CLIENT_SIDE_TIMEOUT,
|
ERR_LDAP_PTA_CONNECT_TIMEOUT.get(host, port,
|
String.valueOf(cfg.dn())), e);
|
}
|
catch (final SSLException e)
|
{
|
if (debugEnabled())
|
{
|
TRACER.debugCaught(DebugLogLevel.ERROR, e);
|
}
|
throw new DirectoryException(ResultCode.CLIENT_SIDE_CONNECT_ERROR,
|
ERR_LDAP_PTA_CONNECT_SSL_ERROR.get(host, port,
|
String.valueOf(cfg.dn()), e.getMessage()), e);
|
}
|
catch (final Exception e)
|
{
|
if (debugEnabled())
|
{
|
TRACER.debugCaught(DebugLogLevel.ERROR, e);
|
}
|
throw new DirectoryException(ResultCode.CLIENT_SIDE_CONNECT_ERROR,
|
ERR_LDAP_PTA_CONNECT_OTHER_ERROR.get(host, port,
|
String.valueOf(cfg.dn()), e.getMessage()), e);
|
}
|
|
}
|
}
|
|
|
|
/**
|
* 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.
|
*/
|
ConnectionFactory getLDAPConnectionFactory(String host, int port,
|
LDAPPassThroughAuthenticationPolicyCfg cfg);
|
|
|
|
/**
|
* 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.
|
*/
|
ScheduledExecutorService getScheduledExecutorService();
|
}
|
|
|
|
/**
|
* A simplistic load-balancer connection factory implementation using
|
* approximately round-robin balancing.
|
*/
|
static final class RoundRobinLoadBalancer extends AbstractLoadBalancer
|
{
|
private final AtomicInteger nextIndex = new AtomicInteger();
|
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.
|
*/
|
RoundRobinLoadBalancer(final ConnectionFactory[] factories,
|
final ScheduledExecutorService scheduler)
|
{
|
super(factories, scheduler);
|
this.maxIndex = factories.length;
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
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
|
{
|
oldNextIndex = nextIndex.get();
|
newNextIndex = oldNextIndex + 1;
|
if (newNextIndex == maxIndex)
|
{
|
newNextIndex = 0;
|
}
|
}
|
while (!nextIndex.compareAndSet(oldNextIndex, newNextIndex));
|
|
// 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.
|
*/
|
private final class PolicyImpl extends AuthenticationPolicy implements
|
ConfigurationChangeListener<LDAPPassThroughAuthenticationPolicyCfg>
|
{
|
|
/**
|
* LDAP PTA policy state implementation.
|
*/
|
private final class StateImpl extends AuthenticationPolicyState
|
{
|
|
private final Entry userEntry;
|
private ByteString cachedPassword = null;
|
|
|
|
private StateImpl(final Entry userEntry)
|
{
|
this.userEntry = userEntry;
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public void finalizeStateAfterBind() throws DirectoryException
|
{
|
if (cachedPassword != null)
|
{
|
// TODO: persist cached password if needed.
|
cachedPassword = null;
|
}
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public AuthenticationPolicy getAuthenticationPolicy()
|
{
|
return PolicyImpl.this;
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public boolean passwordMatches(final ByteString password)
|
throws DirectoryException
|
{
|
sharedLock.lock();
|
try
|
{
|
// First of determine the user name to use when binding to the remote
|
// directory.
|
ByteString username = null;
|
|
switch (cfg.getMappingPolicy())
|
{
|
case UNMAPPED:
|
// The bind DN is the name of the user's entry.
|
username = ByteString.valueOf(userEntry.getDN().toString());
|
break;
|
case MAPPED_BIND:
|
// The bind DN is contained in an attribute in the user's entry.
|
mapBind: for (final AttributeType at : cfg.getMappedAttribute())
|
{
|
final List<Attribute> attributes = userEntry.getAttribute(at);
|
if (attributes != null && !attributes.isEmpty())
|
{
|
for (final Attribute attribute : attributes)
|
{
|
if (!attribute.isEmpty())
|
{
|
username = attribute.iterator().next().getValue();
|
break mapBind;
|
}
|
}
|
}
|
}
|
|
if (username == null)
|
{
|
/*
|
* 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.
|
*/
|
throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
|
ERR_LDAP_PTA_MAPPING_ATTRIBUTE_NOT_FOUND.get(
|
String.valueOf(userEntry.getDN()),
|
String.valueOf(cfg.dn()),
|
mappedAttributesAsString(cfg.getMappedAttribute())));
|
}
|
|
break;
|
case MAPPED_SEARCH:
|
// A search against the remote directory is required in order to
|
// determine the bind DN.
|
|
// Construct the search filter.
|
final LinkedList<SearchFilter> filterComponents =
|
new LinkedList<SearchFilter>();
|
for (final AttributeType at : cfg.getMappedAttribute())
|
{
|
final List<Attribute> attributes = userEntry.getAttribute(at);
|
if (attributes != null && !attributes.isEmpty())
|
{
|
for (final Attribute attribute : attributes)
|
{
|
for (final AttributeValue value : attribute)
|
{
|
filterComponents.add(SearchFilter.createEqualityFilter(at,
|
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.
|
*/
|
throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
|
ERR_LDAP_PTA_MAPPING_ATTRIBUTE_NOT_FOUND.get(
|
String.valueOf(userEntry.getDN()),
|
String.valueOf(cfg.dn()),
|
mappedAttributesAsString(cfg.getMappedAttribute())));
|
}
|
|
final SearchFilter filter;
|
if (filterComponents.size() == 1)
|
{
|
filter = filterComponents.getFirst();
|
}
|
else
|
{
|
filter = SearchFilter.createORFilter(filterComponents);
|
}
|
|
// Now search the configured base DNs, stopping at the first
|
// success.
|
for (final DN baseDN : cfg.getMappedSearchBaseDN())
|
{
|
Connection connection = null;
|
try
|
{
|
connection = searchFactory.getConnection();
|
username = connection.search(baseDN, SearchScope.WHOLE_SUBTREE,
|
filter);
|
}
|
catch (final DirectoryException e)
|
{
|
switch (e.getResultCode())
|
{
|
case NO_SUCH_OBJECT:
|
case CLIENT_SIDE_NO_RESULTS_RETURNED:
|
// Ignore and try next base DN.
|
break;
|
case CLIENT_SIDE_MORE_RESULTS_TO_RETURN:
|
// More than one matching entry was returned.
|
throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
|
ERR_LDAP_PTA_MAPPED_SEARCH_TOO_MANY_CANDIDATES.get(
|
String.valueOf(userEntry.getDN()),
|
String.valueOf(cfg.dn()), String.valueOf(baseDN),
|
String.valueOf(filter)));
|
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.
|
throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
|
ERR_LDAP_PTA_MAPPED_SEARCH_FAILED.get(
|
String.valueOf(userEntry.getDN()),
|
String.valueOf(cfg.dn()), e.getMessageObject()), e);
|
}
|
}
|
finally
|
{
|
if (connection != null)
|
{
|
connection.close();
|
}
|
}
|
}
|
|
if (username == null)
|
{
|
/*
|
* No matching entries were found in the remote directory.
|
*/
|
throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
|
ERR_LDAP_PTA_MAPPED_SEARCH_NO_CANDIDATES.get(
|
String.valueOf(userEntry.getDN()),
|
String.valueOf(cfg.dn()), String.valueOf(filter)));
|
}
|
|
break;
|
}
|
|
// Now perform the bind.
|
Connection connection = null;
|
try
|
{
|
connection = bindFactory.getConnection();
|
connection.simpleBind(username, password);
|
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.
|
throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
|
ERR_LDAP_PTA_MAPPED_BIND_FAILED.get(
|
String.valueOf(userEntry.getDN()),
|
String.valueOf(cfg.dn()), e.getMessageObject()), e);
|
}
|
}
|
finally
|
{
|
if (connection != null)
|
{
|
connection.close();
|
}
|
}
|
}
|
finally
|
{
|
sharedLock.unlock();
|
}
|
}
|
}
|
|
|
|
// Guards against configuration changes.
|
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
|
private final ReadLock sharedLock = lock.readLock();
|
private final WriteLock exclusiveLock = lock.writeLock();
|
|
// Current configuration.
|
private LDAPPassThroughAuthenticationPolicyCfg cfg;
|
|
private ConnectionFactory searchFactory = null;
|
private ConnectionFactory bindFactory = null;
|
|
|
|
private PolicyImpl(
|
final LDAPPassThroughAuthenticationPolicyCfg configuration)
|
{
|
initializeConfiguration(configuration);
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public ConfigChangeResult applyConfigurationChange(
|
final LDAPPassThroughAuthenticationPolicyCfg cfg)
|
{
|
exclusiveLock.lock();
|
try
|
{
|
closeConnections();
|
initializeConfiguration(cfg);
|
}
|
finally
|
{
|
exclusiveLock.unlock();
|
}
|
return new ConfigChangeResult(ResultCode.SUCCESS, false);
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public AuthenticationPolicyState createAuthenticationPolicyState(
|
final Entry userEntry, final long time) throws DirectoryException
|
{
|
// The current time is not needed for LDAP PTA.
|
return new StateImpl(userEntry);
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public void finalizeAuthenticationPolicy()
|
{
|
exclusiveLock.lock();
|
try
|
{
|
cfg.removeLDAPPassThroughChangeListener(this);
|
closeConnections();
|
}
|
finally
|
{
|
exclusiveLock.unlock();
|
}
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public DN getDN()
|
{
|
return cfg.dn();
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public boolean isConfigurationChangeAcceptable(
|
final LDAPPassThroughAuthenticationPolicyCfg cfg,
|
final List<Message> unacceptableReasons)
|
{
|
return LDAPPassThroughAuthenticationPolicyFactory.this
|
.isConfigurationAcceptable(cfg, unacceptableReasons);
|
}
|
|
|
|
private void closeConnections()
|
{
|
exclusiveLock.lock();
|
try
|
{
|
if (searchFactory != null)
|
{
|
searchFactory.close();
|
searchFactory = null;
|
}
|
|
if (bindFactory != null)
|
{
|
bindFactory.close();
|
bindFactory = null;
|
}
|
|
}
|
finally
|
{
|
exclusiveLock.unlock();
|
}
|
}
|
|
|
|
private void initializeConfiguration(
|
final LDAPPassThroughAuthenticationPolicyCfg cfg)
|
{
|
this.cfg = cfg;
|
|
// 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.
|
final RoundRobinLoadBalancer primarySearchLoadBalancer;
|
final RoundRobinLoadBalancer primaryBindLoadBalancer;
|
final ScheduledExecutorService scheduler = provider
|
.getScheduledExecutorService();
|
|
Set<String> servers = cfg.getPrimaryRemoteLDAPServer();
|
ConnectionPool[] searchPool = new ConnectionPool[servers.size()];
|
ConnectionPool[] bindPool = new ConnectionPool[servers.size()];
|
int index = 0;
|
for (final String hostPort : servers)
|
{
|
final ConnectionFactory factory = newLDAPConnectionFactory(hostPort);
|
searchPool[index] = new ConnectionPool(
|
new AuthenticatedConnectionFactory(factory,
|
cfg.getMappedSearchBindDN(),
|
cfg.getMappedSearchBindPassword()));
|
bindPool[index++] = new ConnectionPool(factory);
|
}
|
primarySearchLoadBalancer = new RoundRobinLoadBalancer(searchPool,
|
scheduler);
|
primaryBindLoadBalancer = new RoundRobinLoadBalancer(bindPool, scheduler);
|
|
// Create load-balancers for secondary servers.
|
servers = cfg.getSecondaryRemoteLDAPServer();
|
if (servers.isEmpty())
|
{
|
searchFactory = primarySearchLoadBalancer;
|
bindFactory = primaryBindLoadBalancer;
|
}
|
else
|
{
|
searchPool = new ConnectionPool[servers.size()];
|
bindPool = new ConnectionPool[servers.size()];
|
index = 0;
|
for (final String hostPort : servers)
|
{
|
final ConnectionFactory factory = newLDAPConnectionFactory(hostPort);
|
searchPool[index] = new ConnectionPool(
|
new AuthenticatedConnectionFactory(factory,
|
cfg.getMappedSearchBindDN(),
|
cfg.getMappedSearchBindPassword()));
|
bindPool[index++] = new ConnectionPool(factory);
|
}
|
final RoundRobinLoadBalancer secondarySearchLoadBalancer =
|
new RoundRobinLoadBalancer(searchPool, scheduler);
|
final RoundRobinLoadBalancer secondaryBindLoadBalancer =
|
new RoundRobinLoadBalancer(bindPool, scheduler);
|
searchFactory = new FailoverLoadBalancer(primarySearchLoadBalancer,
|
secondarySearchLoadBalancer, scheduler);
|
bindFactory = new FailoverLoadBalancer(primaryBindLoadBalancer,
|
secondaryBindLoadBalancer, scheduler);
|
}
|
}
|
|
|
|
private ConnectionFactory newLDAPConnectionFactory(final String hostPort)
|
{
|
// Validation already performed by admin framework.
|
final int colonIndex = hostPort.lastIndexOf(":");
|
final String hostname = hostPort.substring(0, colonIndex);
|
final int port = Integer.parseInt(hostPort.substring(colonIndex + 1));
|
return provider.getLDAPConnectionFactory(hostname, port, cfg);
|
}
|
|
}
|
|
|
|
// Debug tracer for this class.
|
private static final DebugTracer TRACER = DebugLogger.getTracer();
|
|
/**
|
* Attribute list for searches requesting no attributes.
|
*/
|
static final LinkedHashSet<String> NO_ATTRIBUTES;
|
|
static
|
{
|
NO_ATTRIBUTES = new LinkedHashSet<String>(1);
|
NO_ATTRIBUTES.add("1.1");
|
}
|
|
// The provider which should be used by policies to create LDAP connections.
|
private final Provider provider;
|
|
/**
|
* The default LDAP connection factory provider.
|
*/
|
private static final Provider DEFAULT_PROVIDER = new Provider()
|
{
|
|
// Global scheduler used for periodically monitoring connection factories in
|
// order to detect when they are online.
|
private final ScheduledExecutorService scheduler = Executors
|
.newScheduledThreadPool(2, new ThreadFactory()
|
{
|
|
@Override
|
public Thread newThread(final Runnable r)
|
{
|
final Thread t = new DirectoryThread(r,
|
"LDAP PTA connection monitor thread");
|
t.setDaemon(true);
|
return t;
|
}
|
});
|
|
|
|
@Override
|
public ConnectionFactory getLDAPConnectionFactory(final String host,
|
final int port, final LDAPPassThroughAuthenticationPolicyCfg cfg)
|
{
|
return new LDAPConnectionFactory(host, port, cfg);
|
}
|
|
|
|
@Override
|
public ScheduledExecutorService getScheduledExecutorService()
|
{
|
return scheduler;
|
}
|
|
};
|
|
|
|
/**
|
* 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.
|
*/
|
static boolean isFatalResultCode(final ResultCode resultCode)
|
{
|
switch (resultCode)
|
{
|
case BUSY:
|
case UNAVAILABLE:
|
case PROTOCOL_ERROR:
|
case OTHER:
|
case UNWILLING_TO_PERFORM:
|
case OPERATIONS_ERROR:
|
case TIME_LIMIT_EXCEEDED:
|
case CLIENT_SIDE_CONNECT_ERROR:
|
case CLIENT_SIDE_DECODING_ERROR:
|
case CLIENT_SIDE_ENCODING_ERROR:
|
case CLIENT_SIDE_LOCAL_ERROR:
|
case CLIENT_SIDE_SERVER_DOWN:
|
case CLIENT_SIDE_TIMEOUT:
|
return true;
|
default:
|
return false;
|
}
|
}
|
|
|
|
private static boolean isServerAddressValid(
|
final LDAPPassThroughAuthenticationPolicyCfg configuration,
|
final List<Message> unacceptableReasons, final String hostPort)
|
{
|
final int colonIndex = hostPort.lastIndexOf(":");
|
final int port = Integer.parseInt(hostPort.substring(colonIndex + 1));
|
if (port < 1 || port > 65535)
|
{
|
if (unacceptableReasons != null)
|
{
|
final Message msg = ERR_LDAP_PTA_INVALID_PORT_NUMBER.get(
|
String.valueOf(configuration.dn()), hostPort);
|
unacceptableReasons.add(msg);
|
}
|
return false;
|
}
|
return true;
|
}
|
|
|
|
private static String mappedAttributesAsString(
|
final Collection<AttributeType> attributes)
|
{
|
switch (attributes.size())
|
{
|
case 0:
|
return "";
|
case 1:
|
return attributes.iterator().next().getNameOrOID();
|
default:
|
final StringBuilder builder = new StringBuilder();
|
final Iterator<AttributeType> i = attributes.iterator();
|
builder.append(i.next().getNameOrOID());
|
while (i.hasNext())
|
{
|
builder.append(", ");
|
builder.append(i.next().getNameOrOID());
|
}
|
return builder.toString();
|
}
|
}
|
|
|
|
/**
|
* Public default constructor used by the admin framework. This will use the
|
* default LDAP connection factory provider.
|
*/
|
public LDAPPassThroughAuthenticationPolicyFactory()
|
{
|
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.
|
*/
|
LDAPPassThroughAuthenticationPolicyFactory(final Provider provider)
|
{
|
this.provider = provider;
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public AuthenticationPolicy createAuthenticationPolicy(
|
final LDAPPassThroughAuthenticationPolicyCfg configuration)
|
throws ConfigException, InitializationException
|
{
|
final PolicyImpl policy = new PolicyImpl(configuration);
|
configuration.addLDAPPassThroughChangeListener(policy);
|
return policy;
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public boolean isConfigurationAcceptable(
|
final LDAPPassThroughAuthenticationPolicyCfg configuration,
|
final List<Message> unacceptableReasons)
|
{
|
// 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;
|
|
for (final String hostPort : configuration.getPrimaryRemoteLDAPServer())
|
{
|
configurationIsAcceptable &= isServerAddressValid(configuration,
|
unacceptableReasons, hostPort);
|
}
|
|
for (final String hostPort : configuration.getSecondaryRemoteLDAPServer())
|
{
|
configurationIsAcceptable &= isServerAddressValid(configuration,
|
unacceptableReasons, hostPort);
|
}
|
|
return configurationIsAcceptable;
|
}
|
}
|