/*
* 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-2013 ForgeRock AS.
*/
package org.opends.server.extensions;
import static org.opends.messages.ExtensionMessages.*;
import static org.opends.server.config.ConfigConstants.*;
import static org.opends.server.loggers.debug.DebugLogger.debugEnabled;
import static org.opends.server.protocols.ldap.LDAPConstants.*;
import static org.opends.server.util.StaticUtils.getExceptionMessage;
import static org.opends.server.util.StaticUtils.getFileForPath;
import java.io.*;
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.meta.
LDAPPassThroughAuthenticationPolicyCfgDefn.MappingPolicy;
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.core.ModifyOperation;
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.internal.InternalClientConnection;
import org.opends.server.protocols.ldap.*;
import org.opends.server.schema.GeneralizedTimeSyntax;
import org.opends.server.schema.SchemaConstants;
import org.opends.server.schema.UserPasswordSyntax;
import org.opends.server.tools.LDAPReader;
import org.opends.server.tools.LDAPWriter;
import org.opends.server.types.*;
import org.opends.server.util.TimeThread;
/**
* LDAP pass through authentication policy implementation.
*/
public final class LDAPPassThroughAuthenticationPolicyFactory implements
AuthenticationPolicyFactory<LDAPPassThroughAuthenticationPolicyCfg>
{
// 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.
*/
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;
do
{
factory = factories[nextIndex];
if (factory.isAvailable)
{
try
{
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;
}
}
else
{
lastException = factory.lastException;
}
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 (!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;
factory.isAvailable = false; // publishes lastException
while (nextIndex != startIndex)
{
factory = factories[nextIndex];
if (factory.isAvailable)
{
try
{
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;
// isAvailable acts as memory barrier for lastException.
private volatile boolean isAvailable = true;
private DirectoryException lastException = null;
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);
}
lastException = e;
isAvailable = false; // publishes lastException
throw e;
}
}
}
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 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);
}
connection = null;
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 e1)
{
// Fail immediately if the result indicates that the operation failed
// for a reason other than connection/server failure.
reconnectIfConnectionFailure(e1);
// The connection has failed, so retry the operation using the new
// connection.
try
{
return connection.search(baseDN, scope, filter);
}
catch (final DirectoryException e2)
{
// If the connection has failed again then give up: don't put the
// connection back in the pool.
closeIfConnectionFailure(e2);
throw e2;
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void simpleBind(final ByteString username,
final ByteString password) throws DirectoryException
{
try
{
connection.simpleBind(username, password);
}
catch (final DirectoryException e1)
{
// Fail immediately if the result indicates that the operation failed
// for a reason other than connection/server failure.
reconnectIfConnectionFailure(e1);
// The connection has failed, so retry the operation using the new
// connection.
try
{
connection.simpleBind(username, password);
}
catch (final DirectoryException e2)
{
// If the connection has failed again then give up: don't put the
// connection back in the pool.
closeIfConnectionFailure(e2);
throw e2;
}
}
}
private void closeIfConnectionFailure(final DirectoryException e)
throws DirectoryException
{
if (isServiceError(e.getResultCode()))
{
connectionIsClosed = true;
connection.close();
connection = null;
availableConnections.release();
}
}
private void reconnectIfConnectionFailure(final DirectoryException e)
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
{
connection = factory.getConnection();
}
catch (final DirectoryException e2)
{
// Give up - the server is unreachable.
connectionIsClosed = true;
connection = null;
availableConnections.release();
throw e2;
}
}
}
// 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))
{
ResultCode resultCode = ResultCode.valueOf(extendedResponse
.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.
*/
ResultCode mappedResultCode = isServiceError(resultCode) ?
resultCode : ResultCode.UNAVAILABLE;
throw new DirectoryException(mappedResultCode,
ERR_LDAP_PTA_CONNECTION_DISCONNECTING.get(host, port,
String.valueOf(cfg.dn()), resultCode.getIntValue(),
resultCode.getResultCodeName(),
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();
/**
* 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.
*/
String getCurrentTime();
/**
* 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.
*/
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 AttributeType cachedPasswordAttribute;
private final AttributeType cachedPasswordTimeAttribute;
private ByteString newCachedPassword = null;
private StateImpl(final Entry userEntry)
{
super(userEntry);
this.cachedPasswordAttribute = DirectoryServer.getAttributeType(
OP_ATTR_PTAPOLICY_CACHED_PASSWORD, true);
this.cachedPasswordTimeAttribute = DirectoryServer.getAttributeType(
OP_ATTR_PTAPOLICY_CACHED_PASSWORD_TIME, true);
}
/**
* {@inheritDoc}
*/
@Override
public void finalizeStateAfterBind() throws DirectoryException
{
sharedLock.lock();
try
{
if (cfg.isUsePasswordCaching() && newCachedPassword != null)
{
// Update the user's entry to contain the cached password and
// time stamp.
ByteString encodedPassword = pwdStorageScheme
.encodePasswordWithScheme(newCachedPassword);
List<RawModification> modifications =
new ArrayList<RawModification>(2);
modifications.add(RawModification.create(ModificationType.REPLACE,
OP_ATTR_PTAPOLICY_CACHED_PASSWORD, encodedPassword));
modifications.add(RawModification.create(ModificationType.REPLACE,
OP_ATTR_PTAPOLICY_CACHED_PASSWORD_TIME,
provider.getCurrentTime()));
InternalClientConnection conn = InternalClientConnection
.getRootConnection();
ModifyOperation internalModify = conn.processModify(userEntry
.getDN().toString(), modifications);
ResultCode resultCode = internalModify.getResultCode();
if (resultCode != ResultCode.SUCCESS)
{
// 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())
{
TRACER.debugWarning(
"An error occurred while trying to update the LDAP PTA "
+ "cached password for user %s: %s", userEntry.getDN()
.toString(), String.valueOf(internalModify
.getErrorMessage()));
}
}
newCachedPassword = null;
}
}
finally
{
sharedLock.unlock();
}
}
/**
* {@inheritDoc}
*/
@Override
public AuthenticationPolicy getAuthenticationPolicy()
{
return PolicyImpl.this;
}
/**
* {@inheritDoc}
*/
@Override
public boolean passwordMatches(final ByteString password)
throws DirectoryException
{
sharedLock.lock();
try
{
// First check the cached password if enabled and available.
if (passwordMatchesCachedPassword(password))
{
return true;
}
// The cache lookup failed, so perform full PTA.
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);
// 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.
newCachedPassword = 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();
}
}
private boolean passwordMatchesCachedPassword(ByteString password)
{
if (!cfg.isUsePasswordCaching())
{
return false;
}
// First determine if the cached password time is present and valid.
boolean foundValidCachedPasswordTime = false;
List<Attribute> cptlist = userEntry
.getAttribute(cachedPasswordTimeAttribute);
if (cptlist != null && !cptlist.isEmpty())
{
foundCachedPasswordTime:
{
for (Attribute attribute : cptlist)
{
// Ignore any attributes with options.
if (!attribute.hasOptions())
{
for (AttributeValue value : attribute)
{
try
{
long cachedPasswordTime = GeneralizedTimeSyntax
.decodeGeneralizedTimeValue(value.getNormalizedValue());
long currentTime = provider.getCurrentTimeMS();
long expiryTime = cachedPasswordTime
+ (cfg.getCachedPasswordTTL() * 1000);
foundValidCachedPasswordTime = (expiryTime > currentTime);
}
catch (DirectoryException e)
{
// Fall-through and give up immediately.
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
}
break foundCachedPasswordTime;
}
}
}
}
}
if (!foundValidCachedPasswordTime)
{
// 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.
ByteString cachedPassword = null;
List<Attribute> cplist = userEntry
.getAttribute(cachedPasswordAttribute);
if (cplist != null && !cplist.isEmpty())
{
foundCachedPassword:
{
for (Attribute attribute : cplist)
{
// Ignore any attributes with options.
if (!attribute.hasOptions())
{
for (AttributeValue value : attribute)
{
cachedPassword = value.getValue();
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
{
String[] userPwComponents = UserPasswordSyntax
.decodeUserPassword(cachedPassword.toString());
PasswordStorageScheme<?> scheme = DirectoryServer
.getPasswordStorageScheme(userPwComponents[0]);
if (scheme != null)
{
return scheme.passwordMatches(password,
ByteString.valueOf(userPwComponents[1]));
}
}
catch (DirectoryException e)
{
// Unable to decode the cached password, so give up.
if (debugEnabled())
{
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
}
return false;
}
}
// 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 PasswordStorageScheme<?> pwdStorageScheme = 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;
// First obtain the mapped search password if needed, ignoring any errors
// since these should have already been detected during configuration
// validation.
final String mappedSearchPassword;
if (cfg.getMappingPolicy() == MappingPolicy.MAPPED_SEARCH
&& cfg.getMappedSearchBindDN() != null
&& !cfg.getMappedSearchBindDN().isNullDN())
{
mappedSearchPassword = getMappedSearchBindPassword(cfg,
new LinkedList<Message>());
}
else
{
mappedSearchPassword = null;
}
// 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(),
mappedSearchPassword));
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(),
mappedSearchPassword));
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);
}
if (cfg.isUsePasswordCaching())
{
pwdStorageScheme = DirectoryServer.getPasswordStorageScheme(cfg
.getCachedPasswordStorageSchemeDN());
}
}
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 = new LinkedHashSet<String>(
1);
static
{
NO_ATTRIBUTES.add(SchemaConstants.NO_ATTRIBUTES);
}
// 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;
}
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.
*/
static boolean isServiceError(final ResultCode resultCode)
{
switch (resultCode)
{
case OPERATIONS_ERROR:
case PROTOCOL_ERROR:
case TIME_LIMIT_EXCEEDED:
case ADMIN_LIMIT_EXCEEDED:
case UNAVAILABLE_CRITICAL_EXTENSION:
case BUSY:
case UNAVAILABLE:
case UNWILLING_TO_PERFORM:
case LOOP_DETECT:
case OTHER:
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;
}
}
//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.
private static String getMappedSearchBindPassword(
final LDAPPassThroughAuthenticationPolicyCfg cfg,
final List<Message> unacceptableReasons)
{
String password = null;
if (cfg.getMappedSearchBindPasswordProperty() != null)
{
String propertyName = cfg.getMappedSearchBindPasswordProperty();
password = System.getProperty(propertyName);
if (password == null)
{
unacceptableReasons.add(ERR_LDAP_PTA_PWD_PROPERTY_NOT_SET.get(
String.valueOf(cfg.dn()), String.valueOf(propertyName)));
}
}
else if (cfg.getMappedSearchBindPasswordEnvironmentVariable() != null)
{
String envVarName = cfg.getMappedSearchBindPasswordEnvironmentVariable();
password = System.getenv(envVarName);
if (password == null)
{
unacceptableReasons.add(ERR_LDAP_PTA_PWD_ENVAR_NOT_SET.get(
String.valueOf(cfg.dn()), String.valueOf(envVarName)));
}
}
else if (cfg.getMappedSearchBindPasswordFile() != null)
{
String fileName = cfg.getMappedSearchBindPasswordFile();
File passwordFile = getFileForPath(fileName);
if (!passwordFile.exists())
{
unacceptableReasons.add(ERR_LDAP_PTA_PWD_NO_SUCH_FILE.get(
String.valueOf(cfg.dn()), String.valueOf(fileName)));
}
else
{
BufferedReader br = null;
try
{
br = new BufferedReader(new FileReader(passwordFile));
password = br.readLine();
if (password == null)
{
unacceptableReasons.add(ERR_LDAP_PTA_PWD_FILE_EMPTY.get(
String.valueOf(cfg.dn()), String.valueOf(fileName)));
}
}
catch (IOException e)
{
unacceptableReasons.add(ERR_LDAP_PTA_PWD_FILE_CANNOT_READ.get(
String.valueOf(cfg.dn()), String.valueOf(fileName),
getExceptionMessage(e)));
}
finally
{
try
{
br.close();
}
catch (Exception e)
{
// Ignored.
}
}
}
}
else if (cfg.getMappedSearchBindPassword() != null)
{
password = cfg.getMappedSearchBindPassword();
}
else
{
// Password wasn't defined anywhere.
unacceptableReasons
.add(ERR_LDAP_PTA_NO_PWD.get(String.valueOf(cfg.dn())));
}
return password;
}
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 cfg,
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 : cfg.getPrimaryRemoteLDAPServer())
{
configurationIsAcceptable &= isServerAddressValid(cfg,
unacceptableReasons, hostPort);
}
for (final String hostPort : cfg.getSecondaryRemoteLDAPServer())
{
configurationIsAcceptable &= isServerAddressValid(cfg,
unacceptableReasons, hostPort);
}
// Ensure that the search bind password is defined somewhere.
if (cfg.getMappingPolicy() == MappingPolicy.MAPPED_SEARCH
&& cfg.getMappedSearchBindDN() != null
&& !cfg.getMappedSearchBindDN().isNullDN())
{
if (getMappedSearchBindPassword(cfg, unacceptableReasons) == null)
{
configurationIsAcceptable = false;
}
}
return configurationIsAcceptable;
}
}