/*
* 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 legal-notices/CDDLv1_0.txt
* or http://forgerock.org/license/CDDLv1.0.html.
* 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 legal-notices/CDDLv1_0.txt.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Portions Copyright 2011-2015 ForgeRock AS.
*/
package org.opends.server.extensions;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.*;
import java.util.concurrent.*;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.io.ASN1;
import org.forgerock.opendj.io.ASN1Writer;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ByteStringBuilder;
import org.forgerock.opendj.ldap.DereferenceAliasesPolicy;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchScope;
import org.opends.server.TestCaseUtils;
import org.opends.server.admin.server.ConfigurationChangeListener;
import org.opends.server.admin.std.meta.LDAPPassThroughAuthenticationPolicyCfgDefn.MappingPolicy;
import org.opends.server.admin.std.server.AuthenticationPolicyCfg;
import org.opends.server.admin.std.server.LDAPPassThroughAuthenticationPolicyCfg;
import org.opends.server.api.AuthenticationPolicy;
import org.opends.server.api.AuthenticationPolicyState;
import org.opends.server.core.DirectoryServer;
import org.opends.server.extensions.LDAPPassThroughAuthenticationPolicyFactory.Connection;
import org.opends.server.extensions.LDAPPassThroughAuthenticationPolicyFactory.ConnectionFactory;
import org.opends.server.extensions.LDAPPassThroughAuthenticationPolicyFactory.ConnectionPool;
import org.opends.server.extensions.LDAPPassThroughAuthenticationPolicyFactory.LDAPConnectionFactory;
import org.opends.server.protocols.ldap.*;
import org.opends.server.schema.GeneralizedTimeSyntax;
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.StaticUtils;
import org.opends.server.util.TimeThread;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static org.opends.server.extensions.LDAPPassThroughAuthenticationPolicyFactory.*;
import static org.opends.server.protocols.ldap.LDAPConstants.*;
import static org.opends.server.util.StaticUtils.*;
import static org.testng.Assert.*;
/**
* Test LDAP authentication mappingPolicy implementation.
*/
@SuppressWarnings("javadoc")
public class LDAPPassThroughAuthenticationPolicyTestCase extends
ExtensionsTestCase
{
static class CloseEvent extends Event<Void>
{
private final GetConnectionEvent getConnectionEvent;
CloseEvent(final GetConnectionEvent getConnectionEvent)
{
this.getConnectionEvent = getConnectionEvent;
}
/** {@inheritDoc} */
@Override
boolean matchesEvent(final Event<?> event)
{
if (event instanceof CloseEvent)
{
final CloseEvent closeEvent = (CloseEvent) event;
return getConnectionEvent.matchesEvent(closeEvent.getConnectionEvent);
}
return false;
}
/** {@inheritDoc} */
@Override
StringBuilder toString(final StringBuilder builder)
{
builder.append("CloseEvent(");
builder.append(getConnectionEvent);
builder.append(')');
return builder;
}
}
static abstract class Event<T>
{
@Override
public final boolean equals(final Object obj)
{
if (obj instanceof Event<?>)
{
return matchesEvent((Event<?>) obj);
}
return false;
}
@Override
public final String toString()
{
final StringBuilder builder = new StringBuilder();
return toString(builder).toString();
}
T getResult()
{
return null;
}
abstract boolean matchesEvent(Event<?> event);
abstract StringBuilder toString(StringBuilder builder);
}
static class GetConnectionEvent extends Event<DirectoryException>
{
private final GetLDAPConnectionFactoryEvent fevent;
private final ResultCode resultCode;
GetConnectionEvent(final GetLDAPConnectionFactoryEvent fevent)
{
this(fevent, ResultCode.SUCCESS);
}
GetConnectionEvent(final GetLDAPConnectionFactoryEvent fevent,
final ResultCode resultCode)
{
this.fevent = fevent;
this.resultCode = resultCode;
}
/** {@inheritDoc} */
@Override
DirectoryException getResult()
{
if (resultCode != ResultCode.SUCCESS)
{
return new DirectoryException(resultCode, resultCode.getName());
}
return null;
}
/** {@inheritDoc} */
@Override
boolean matchesEvent(final Event<?> event)
{
if (event instanceof GetConnectionEvent)
{
final GetConnectionEvent getConnectionEvent = (GetConnectionEvent) event;
return fevent.matchesEvent(getConnectionEvent.fevent);
}
return false;
}
/** {@inheritDoc} */
@Override
StringBuilder toString(final StringBuilder builder)
{
builder.append("GetConnectionEvent(");
builder.append(fevent);
builder.append(')');
return builder;
}
}
static class GetLDAPConnectionFactoryEvent extends Event<Void>
{
private final String hostPort;
private final LDAPPassThroughAuthenticationPolicyCfg options;
GetLDAPConnectionFactoryEvent(final String hostPort,
final LDAPPassThroughAuthenticationPolicyCfg options)
{
this.hostPort = hostPort;
this.options = options;
}
/** {@inheritDoc} */
@Override
boolean matchesEvent(final Event<?> event)
{
if (event instanceof GetLDAPConnectionFactoryEvent)
{
final GetLDAPConnectionFactoryEvent providerEvent = (GetLDAPConnectionFactoryEvent) event;
return hostPort.equals(providerEvent.hostPort)
&& options == providerEvent.options;
}
return false;
}
/** {@inheritDoc} */
@Override
StringBuilder toString(final StringBuilder builder)
{
builder.append("GetLDAPConnectionFactoryEvent(");
builder.append(hostPort);
builder.append(')');
return builder;
}
}
static final class MockConnection implements
LDAPPassThroughAuthenticationPolicyFactory.Connection
{
private final GetConnectionEvent getConnectionEvent;
private final MockProvider mockProvider;
MockConnection(final MockProvider mockProvider,
final GetConnectionEvent getConnectionEvent)
{
this.mockProvider = mockProvider;
this.getConnectionEvent = getConnectionEvent;
}
/** {@inheritDoc} */
@Override
public void close()
{
final CloseEvent event = new CloseEvent(getConnectionEvent);
mockProvider.assertExpectedEventWasReceived(event);
}
/** {@inheritDoc} */
@Override
public ByteString search(final DN baseDN, final SearchScope scope,
final SearchFilter filter) throws DirectoryException
{
final SearchEvent event = new SearchEvent(getConnectionEvent,
baseDN.toString(), scope, filter.toString());
final Object result = mockProvider.assertExpectedEventWasReceived(event);
if (result instanceof String)
{
return ByteString.valueOfUtf8((String) result);
}
else
{
throw (DirectoryException) result;
}
}
/** {@inheritDoc} */
@Override
public void simpleBind(final ByteString username, final ByteString password)
throws DirectoryException
{
final SimpleBindEvent event = new SimpleBindEvent(getConnectionEvent,
username.toString(), password.toString());
final DirectoryException e = mockProvider
.assertExpectedEventWasReceived(event);
if (e != null)
{
throw e;
}
}
}
static final class MockFactory implements
LDAPPassThroughAuthenticationPolicyFactory.ConnectionFactory
{
private final GetLDAPConnectionFactoryEvent getLDAPConnectionFactoryEvent;
private final MockProvider mockProvider;
MockFactory(final MockProvider mockProvider,
final GetLDAPConnectionFactoryEvent providerEvent)
{
this.mockProvider = mockProvider;
this.getLDAPConnectionFactoryEvent = providerEvent;
}
/** {@inheritDoc} */
@Override
public void close()
{
// Nothing to do.
}
/** {@inheritDoc} */
@Override
public Connection getConnection() throws DirectoryException
{
final GetConnectionEvent event = new GetConnectionEvent(
getLDAPConnectionFactoryEvent);
final DirectoryException e = mockProvider
.assertExpectedEventWasReceived(event);
if (e != null)
{
throw e;
}
else
{
return new MockConnection(mockProvider, event);
}
}
}
final class MockPolicyCfg implements LDAPPassThroughAuthenticationPolicyCfg
{
private final SortedSet<DN> baseDNs = new TreeSet<>();
private final SortedSet<AttributeType> mappedAttributes = new TreeSet<>();
private MappingPolicy mappingPolicy = MappingPolicy.UNMAPPED;
private final SortedSet<String> primaryServers = new TreeSet<>();
private final SortedSet<String> secondaryServers = new TreeSet<>();
/** Unlimited. */
private int timeoutMS;
private DN mappedSearchBindDN = searchBindDN;
private String mappedSearchBindPassword = "searchPassword";
private String mappedSearchBindPasswordEnvVar;
private String mappedSearchBindPasswordFile;
private String mappedSearchBindPasswordProperty;
private boolean usePasswordCaching;
@Override
public void addChangeListener(
final ConfigurationChangeListener<AuthenticationPolicyCfg> listener)
{
// Do nothing.
}
@Override
public void addLDAPPassThroughChangeListener(
final ConfigurationChangeListener<LDAPPassThroughAuthenticationPolicyCfg> listener)
{
// Do nothing.
}
@Override
public Class<? extends LDAPPassThroughAuthenticationPolicyCfg> configurationClass()
{
return LDAPPassThroughAuthenticationPolicyCfg.class;
}
@Override
public DN dn()
{
return policyDN;
}
@Override
public long getConnectionTimeout()
{
return timeoutMS;
}
@Override
public InetAddress getSourceAddress() { return null; }
@Override
public String getJavaClass()
{
return LDAPPassThroughAuthenticationPolicyFactory.class.getName();
}
@Override
public SortedSet<AttributeType> getMappedAttribute()
{
return mappedAttributes;
}
@Override
public SortedSet<DN> getMappedSearchBaseDN()
{
return baseDNs;
}
@Override
public DN getMappedSearchBindDN()
{
return mappedSearchBindDN;
}
@Override
public String getMappedSearchBindPassword()
{
return mappedSearchBindPassword;
}
@Override
public MappingPolicy getMappingPolicy()
{
return mappingPolicy;
}
@Override
public SortedSet<String> getPrimaryRemoteLDAPServer()
{
return primaryServers;
}
@Override
public SortedSet<String> getSecondaryRemoteLDAPServer()
{
return secondaryServers;
}
@Override
public SortedSet<String> getSSLCipherSuite()
{
return new TreeSet<>();
}
@Override
public SortedSet<String> getSSLProtocol()
{
return new TreeSet<>();
}
@Override
public String getTrustManagerProvider()
{
return "ignored";
}
@Override
public DN getTrustManagerProviderDN()
{
return trustManagerDN;
}
@Override
public boolean isUseSSL()
{
return false;
}
@Override
public boolean isUseTCPKeepAlive()
{
return false;
}
@Override
public boolean isUseTCPNoDelay()
{
return false;
}
@Override
public void removeChangeListener(
final ConfigurationChangeListener<AuthenticationPolicyCfg> listener)
{
// Do nothing.
}
@Override
public void removeLDAPPassThroughChangeListener(
final ConfigurationChangeListener<LDAPPassThroughAuthenticationPolicyCfg> listener)
{
// Do nothing.
}
MockPolicyCfg withBaseDN(final String baseDN)
{
try
{
baseDNs.add(DN.valueOf(baseDN));
}
catch (final DirectoryException e)
{
throw new RuntimeException(e);
}
return this;
}
MockPolicyCfg withConnectionTimeout(final int timeoutMS)
{
this.timeoutMS = timeoutMS;
return this;
}
MockPolicyCfg withMappedAttribute(final String atype)
{
AttributeType attrType = DirectoryServer.getAttributeTypeOrDefault(toLowerCase(atype));
mappedAttributes.add(attrType);
return this;
}
MockPolicyCfg withMappingPolicy(final MappingPolicy policy)
{
this.mappingPolicy = policy;
return this;
}
MockPolicyCfg withPrimaryServer(final String hostPort)
{
primaryServers.add(hostPort);
return this;
}
MockPolicyCfg withSecondaryServer(final String hostPort)
{
secondaryServers.add(hostPort);
return this;
}
MockPolicyCfg withMappedSearchBindDN(final DN value)
{
this.mappedSearchBindDN = value;
return this;
}
MockPolicyCfg withMappedSearchBindPassword(final String value)
{
this.mappedSearchBindPassword = value;
return this;
}
MockPolicyCfg withMappedSearchBindPasswordEnvironmentVariable(final String value)
{
this.mappedSearchBindPasswordEnvVar = value;
return this;
}
MockPolicyCfg withMappedSearchBindPasswordFile(final String value)
{
this.mappedSearchBindPasswordFile = value;
return this;
}
MockPolicyCfg withMappedSearchBindPasswordProperty(final String value)
{
this.mappedSearchBindPasswordProperty = value;
return this;
}
MockPolicyCfg withUsePasswordCaching(final boolean usePasswordCaching)
{
this.usePasswordCaching = usePasswordCaching;
return this;
}
/** {@inheritDoc} */
@Override
public String getMappedSearchBindPasswordEnvironmentVariable()
{
return mappedSearchBindPasswordEnvVar;
}
/** {@inheritDoc} */
@Override
public String getMappedSearchBindPasswordFile()
{
return mappedSearchBindPasswordFile;
}
/** {@inheritDoc} */
@Override
public String getMappedSearchBindPasswordProperty()
{
return mappedSearchBindPasswordProperty;
}
/** {@inheritDoc} */
@Override
public long getCachedPasswordTTL()
{
return 86400;
}
/** {@inheritDoc} */
@Override
public String getCachedPasswordStorageScheme()
{
return "Salted SHA-1";
}
/** {@inheritDoc} */
@Override
public DN getCachedPasswordStorageSchemeDN()
{
try
{
return DN.valueOf("cn=Salted SHA-1,cn=Password Storage Schemes,cn=config");
}
catch (DirectoryException e)
{
throw new RuntimeException(e);
}
}
/** {@inheritDoc} */
@Override
public boolean isUsePasswordCaching()
{
return usePasswordCaching;
}
}
static final class MockProvider implements
LDAPPassThroughAuthenticationPolicyFactory.Provider
{
private final class MockScheduledFuture implements ScheduledFuture<Void>
{
private final Runnable runnable;
MockScheduledFuture(final Runnable runnable)
{
this.runnable = runnable;
}
@Override
public boolean cancel(final boolean mayInterruptIfRunning)
{
monitorRunnables.remove(this);
return true;
}
@Override
public int compareTo(final Delayed o)
{
return 0;
}
@Override
public Void get() throws InterruptedException, ExecutionException
{
return null;
}
@Override
public Void get(final long timeout, final TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException
{
return null;
}
@Override
public long getDelay(final TimeUnit unit)
{
return 0;
}
@Override
public boolean isCancelled()
{
return false;
}
@Override
public boolean isDone()
{
return false;
}
Runnable getRunnable()
{
return runnable;
}
}
private final Queue<Event<?>> expectedEvents = new LinkedList<>();
private final List<MockScheduledFuture> monitorRunnables = new LinkedList<>();
private String currentTime = TimeThread.getGMTTime();
/** All methods unused excepted scheduleWithFixedDelay. */
private final ScheduledExecutorService mockScheduler = new ScheduledExecutorService()
{
@Override
public boolean awaitTermination(final long timeout, final TimeUnit unit)
throws InterruptedException
{
throw new UnsupportedOperationException();
}
@Override
public void execute(final Runnable command)
{
throw new UnsupportedOperationException();
}
@Override
public <T> List<Future<T>> invokeAll(
final Collection<? extends Callable<T>> tasks)
throws InterruptedException
{
throw new UnsupportedOperationException();
}
@Override
public <T> List<Future<T>> invokeAll(
final Collection<? extends Callable<T>> tasks, final long timeout,
final TimeUnit unit) throws InterruptedException
{
throw new UnsupportedOperationException();
}
@Override
public <T> T invokeAny(final Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException
{
throw new UnsupportedOperationException();
}
@Override
public <T> T invokeAny(final Collection<? extends Callable<T>> tasks,
final long timeout, final TimeUnit unit) throws InterruptedException,
ExecutionException, TimeoutException
{
throw new UnsupportedOperationException();
}
@Override
public boolean isShutdown()
{
return false;
}
@Override
public boolean isTerminated()
{
return false;
}
@Override
public <V> ScheduledFuture<V> schedule(final Callable<V> callable,
final long delay, final TimeUnit unit)
{
throw new UnsupportedOperationException();
}
@Override
public ScheduledFuture<?> schedule(final Runnable command,
final long delay, final TimeUnit unit)
{
throw new UnsupportedOperationException();
}
@Override
public ScheduledFuture<?> scheduleAtFixedRate(final Runnable command,
final long initialDelay, final long period, final TimeUnit unit)
{
throw new UnsupportedOperationException();
}
@Override
public ScheduledFuture<?> scheduleWithFixedDelay(final Runnable command,
final long initialDelay, final long delay, final TimeUnit unit)
{
final MockScheduledFuture future = new MockScheduledFuture(command);
monitorRunnables.add(future);
return future;
}
@Override
public void shutdown()
{
throw new UnsupportedOperationException();
}
@Override
public List<Runnable> shutdownNow()
{
throw new UnsupportedOperationException();
}
@Override
public <T> Future<T> submit(final Callable<T> task)
{
throw new UnsupportedOperationException();
}
@Override
public Future<?> submit(final Runnable task)
{
throw new UnsupportedOperationException();
}
@Override
public <T> Future<T> submit(final Runnable task, final T result)
{
throw new UnsupportedOperationException();
}
};
/** {@inheritDoc} */
@Override
public ConnectionFactory getLDAPConnectionFactory(final String host,
final int port, final LDAPPassThroughAuthenticationPolicyCfg options)
{
final GetLDAPConnectionFactoryEvent event = new GetLDAPConnectionFactoryEvent(
host + ":" + port, options);
assertExpectedEventWasReceived(event);
return new MockFactory(this, event);
}
/** {@inheritDoc} */
@Override
public ScheduledExecutorService getScheduledExecutorService()
{
return mockScheduler;
}
/** {@inheritDoc} */
@Override
public String getCurrentTime()
{
return currentTime;
}
/** {@inheritDoc} */
@Override
public long getCurrentTimeMS()
{
try
{
return GeneralizedTimeSyntax.decodeGeneralizedTimeValue(ByteString
.valueOfUtf8(currentTime));
}
catch (DirectoryException e)
{
throw new RuntimeException(e);
}
}
void assertAllExpectedEventsReceived()
{
assertTrue(expectedEvents.isEmpty());
}
@SuppressWarnings("unchecked")
<T> T assertExpectedEventWasReceived(final Event<T> actualEvent)
{
final Event<?> expectedEvent = expectedEvents.poll();
if (expectedEvent == null)
{
fail("Unexpected event: " + actualEvent);
}
else
{
assertEquals(actualEvent, expectedEvent);
}
return ((Event<T>) expectedEvent).getResult();
}
MockProvider expectEvent(final Event<?> expectedEvent)
{
expectedEvents.add(expectedEvent);
return this;
}
MockProvider withCurrentTime(final String currentTime)
{
this.currentTime = currentTime;
return this;
}
void runMonitorTasks()
{
for (final MockScheduledFuture task : monitorRunnables)
{
task.getRunnable().run();
}
}
}
final class MockServer
{
/** Waits for an incoming client connection. */
class AcceptAction extends Action
{
@Override
void run() throws Exception
{
accept();
}
}
abstract class Action
{
abstract void run() throws Exception;
}
/** Blocks the server until it is released. */
class BlockAction extends Action
{
private final CountDownLatch latch = new CountDownLatch(1);
void release()
{
latch.countDown();
}
@Override
void run() throws Exception
{
latch.await();
}
}
/** Close the client socket. */
class CloseAction extends Action
{
@Override
void run() throws Exception
{
getSocket().close();
}
}
/** Read the next message and check it matches the expected message. */
class ReceiveAction extends Action
{
private final int messageID;
private final ProtocolOp expectedOp;
ReceiveAction(final int messageID, final ProtocolOp expectedOp)
{
this.messageID = messageID;
this.expectedOp = expectedOp;
}
@Override
void run() throws Exception
{
// Read next message.
final LDAPReader reader = new LDAPReader(getSocket());
final LDAPMessage message = reader.readMessage();
// Check message ID matches.
assertEquals(message.getMessageID(), messageID);
// Check protocol op matches.
final ProtocolOp actualOp = message.getProtocolOp();
final ByteStringBuilder b1 = new ByteStringBuilder();
final ByteStringBuilder b2 = new ByteStringBuilder();
final ASN1Writer w1 = ASN1.getWriter(b1);
final ASN1Writer w2 = ASN1.getWriter(b2);
expectedOp.write(w1);
actualOp.write(w2);
assertEquals(b1, b2);
}
}
/** Sends a message. */
class SendAction extends Action
{
private final int messageID;
private final ProtocolOp op;
SendAction(final int messageID, final ProtocolOp op)
{
this.messageID = messageID;
this.op = op;
}
@Override
void run() throws Exception
{
final LDAPWriter writer = new LDAPWriter(getSocket());
final LDAPMessage message = new LDAPMessage(messageID, op);
writer.writeMessage(message);
}
}
private final ServerSocket serverSocket;
private final List<Action> actions = new LinkedList<>();
private Socket socket;
private volatile Exception e;
private Thread serverThread;
private final CountDownLatch stopLatch = new CountDownLatch(1);
private final Queue<BlockAction> blockers = new LinkedList<>();
MockServer(final ServerSocket serverSocket)
{
this.serverSocket = serverSocket;
}
void assertNoExceptions() throws Exception
{
if (e != null)
{
throw e;
}
}
int getPort()
{
return serverSocket.getLocalPort();
}
MockServer start()
{
serverThread = new Thread(new Runnable()
{
/** {@inheritDoc} */
@Override
public void run()
{
for (final Action action : actions)
{
try
{
action.run();
}
catch (final Exception e)
{
MockServer.this.e = e;
break;
}
}
StaticUtils.close(serverSocket);
StaticUtils.close(socket);
// Release test thread.
stopLatch.countDown();
}
}, "MockServer on port " + serverSocket.getLocalPort());
serverThread.start();
return this;
}
void stop() throws Exception
{
stopLatch.await(10, TimeUnit.SECONDS);
if (serverThread != null)
{
serverThread.interrupt();
}
stopLatch.await();
assertNoExceptions();
}
MockServer thenAccept()
{
actions.add(new AcceptAction());
return this;
}
MockServer thenBlock()
{
final BlockAction action = new BlockAction();
actions.add(action);
blockers.add(action);
return this;
}
MockServer thenClose()
{
actions.add(new CloseAction());
return this;
}
MockServer thenReceive(final int messageID, final ProtocolOp op)
{
actions.add(new ReceiveAction(messageID, op));
return this;
}
MockServer thenSend(final int messageID, final ProtocolOp op)
{
actions.add(new SendAction(messageID, op));
return this;
}
void unblock() throws Exception
{
final BlockAction action = blockers.poll();
assertNotNull(action);
action.release();
}
private Socket accept() throws IOException
{
socket = serverSocket.accept();
return socket;
}
private Socket getSocket()
{
return socket;
}
}
static class SearchEvent extends Event<Object>
{
private final String baseDN;
private final GetConnectionEvent cevent;
private final String filter;
private final ResultCode resultCode;
private final SearchScope scope;
private final String username;
SearchEvent(final GetConnectionEvent cevent, final String baseDN,
final SearchScope scope, final String filter)
{
this(cevent, baseDN, scope, filter, null, ResultCode.SUCCESS);
}
SearchEvent(final GetConnectionEvent cevent, final String baseDN,
final SearchScope scope, final String filter,
final ResultCode resultCode)
{
this(cevent, baseDN, scope, filter, null, resultCode);
}
SearchEvent(final GetConnectionEvent cevent, final String baseDN,
final SearchScope scope, final String filter, final String username)
{
this(cevent, baseDN, scope, filter, username, ResultCode.SUCCESS);
}
private SearchEvent(final GetConnectionEvent cevent, final String baseDN,
final SearchScope scope, final String filter, final String username,
final ResultCode resultCode)
{
this.cevent = cevent;
this.baseDN = baseDN;
this.scope = scope;
this.filter = filter;
this.username = username;
this.resultCode = resultCode;
}
/** {@inheritDoc} */
@Override
Object getResult()
{
return resultCode == ResultCode.SUCCESS ? username
: new DirectoryException(resultCode, resultCode.getName());
}
/** {@inheritDoc} */
@Override
boolean matchesEvent(final Event<?> event)
{
if (event instanceof SearchEvent)
{
final SearchEvent searchEvent = (SearchEvent) event;
return cevent.matchesEvent(searchEvent.cevent)
&& baseDN.equals(searchEvent.baseDN)
&& scope.equals(searchEvent.scope)
&& filter.equals(searchEvent.filter);
}
return false;
}
/** {@inheritDoc} */
@Override
StringBuilder toString(final StringBuilder builder)
{
builder.append("SearchEvent(");
builder.append(cevent);
builder.append(", ");
builder.append(baseDN);
builder.append(", ");
builder.append(scope);
builder.append(", ");
builder.append(filter);
builder.append(')');
return builder;
}
}
static class SimpleBindEvent extends Event<DirectoryException>
{
private final GetConnectionEvent cevent;
private final String password;
private final ResultCode resultCode;
private final String username;
SimpleBindEvent(final GetConnectionEvent cevent, final String username,
final String password)
{
this(cevent, username, password, ResultCode.SUCCESS);
}
SimpleBindEvent(final GetConnectionEvent cevent, final String username,
final String password, final ResultCode resultCode)
{
this.cevent = cevent;
this.username = username;
this.password = password;
this.resultCode = resultCode;
}
/** {@inheritDoc} */
@Override
DirectoryException getResult()
{
if (resultCode != ResultCode.SUCCESS)
{
return new DirectoryException(resultCode,
resultCode.getName());
}
return null;
}
/** {@inheritDoc} */
@Override
boolean matchesEvent(final Event<?> event)
{
if (event instanceof SimpleBindEvent)
{
final SimpleBindEvent simpleBindEvent = (SimpleBindEvent) event;
return cevent.matchesEvent(simpleBindEvent.cevent)
&& username.equals(simpleBindEvent.username)
&& password.equals(simpleBindEvent.password);
}
return false;
}
/** {@inheritDoc} */
@Override
StringBuilder toString(final StringBuilder builder)
{
builder.append("SimpleBindEvent(");
builder.append(cevent);
builder.append(", ");
builder.append(username);
builder.append(", ");
builder.append(password);
builder.append(')');
return builder;
}
}
private final String phost1 = "phost1:11";
private final String phost2 = "phost2:22";
private final String phost3 = "phost3:33";
private final String shost1 = "shost1:11";
private DN policyDN;
private final String policyDNString = "cn=test policy,o=test";
private DN searchBindDN;
private final String searchBindDNString = "cn=search bind dn";
private DN trustManagerDN;
private final String trustManagerDNString = "cn=ignored";
private final String adDNString = "uid=aduser,o=ad";
private final String opendjDNString = "cn=test user,o=opendj";
private Entry userEntry;
private final String userPassword = "password";
/**
* Ensures that the Directory Server is running and creates a test backend
* containing a single test user.
*
* @throws Exception
* If an unexpected problem occurs.
*/
@BeforeClass
public void beforeClass() throws Exception
{
TestCaseUtils.startServer();
policyDN = DN.valueOf(policyDNString);
trustManagerDN = DN.valueOf(trustManagerDNString);
searchBindDN = DN.valueOf(searchBindDNString);
userEntry = TestCaseUtils.makeEntry(
/* @formatter:off */
"dn: " + opendjDNString,
"objectClass: top",
"objectClass: person",
"sn: user",
"cn: test user",
"aduser: " + adDNString,
"uid: aduser"
/* @formatter:on */
);
}
/**
* Tests that failures during the search are handled properly.
* <p>
* Non-fatal errors (e.g. entry not found, too many entries returned) should
* not cause the search connection to be closed.
*
* @param searchResultCode
* The search result code.
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true, dataProvider = "testConnectionFailureDuringSearchData")
public void testConnectionFailureDuringSearch(
final ResultCode searchResultCode) throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
.withPrimaryServer(phost1)
.withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
.withMappedAttribute("uid").withBaseDN("o=ad");
// Create the provider and its list of expected events.
final GetLDAPConnectionFactoryEvent fe = new GetLDAPConnectionFactoryEvent(
phost1, cfg);
final GetConnectionEvent ceSearch = new GetConnectionEvent(fe);
final MockProvider provider = new MockProvider()
.expectEvent(fe)
.expectEvent(ceSearch)
.expectEvent(
new SimpleBindEvent(ceSearch, searchBindDNString, "searchPassword",
ResultCode.SUCCESS))
.expectEvent(
new SearchEvent(ceSearch, "o=ad", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", searchResultCode));
if (isServiceError(searchResultCode))
{
// The connection will fail and be closed immediately, and the pool will
// retry on new connection.
provider.expectEvent(new CloseEvent(ceSearch));
provider.expectEvent(new GetConnectionEvent(fe, searchResultCode));
}
// Obtain policy and state.
final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
provider);
assertTrue(factory.isConfigurationAcceptable(cfg, null));
final AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
final AuthenticationPolicyState state = policy
.createAuthenticationPolicyState(userEntry);
assertEquals(state.getAuthenticationPolicy(), policy);
// Perform authentication.
try
{
state.passwordMatches(ByteString.valueOfUtf8(userPassword));
fail("password match unexpectedly succeeded");
}
catch (final DirectoryException e)
{
// No valid connections available so this should always fail with
// INVALID_CREDENTIALS.
assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS,
e.getMessage());
}
// There should be no more pending events.
provider.assertAllExpectedEventsReceived();
state.finalizeStateAfterBind();
// Cached connections should be closed when the policy is finalized.
if (!isServiceError(searchResultCode))
{
provider.expectEvent(new CloseEvent(ceSearch));
}
// Tear down and check final state.
policy.finalizeAuthenticationPolicy();
provider.assertAllExpectedEventsReceived();
}
/**
* Tests that failures to authenticate a search connection are handled
* properly.
* <p>
* Any kind of failure occurring while trying to authenticate a search
* connection should result in the connection being closed and periodically
* retried.
*
* @param bindResultCode
* The bind result code.
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true, dataProvider = "testConnectionFailureDuringSearchBindData")
public void testConnectionFailureDuringSearchBind(
final ResultCode bindResultCode) throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
.withPrimaryServer(phost1)
.withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
.withMappedAttribute("uid").withBaseDN("o=ad");
// Create the provider and its list of expected events.
final GetLDAPConnectionFactoryEvent fe = new GetLDAPConnectionFactoryEvent(
phost1, cfg);
final GetConnectionEvent ceSearch = new GetConnectionEvent(fe);
final MockProvider provider = new MockProvider()
.expectEvent(fe)
.expectEvent(ceSearch)
.expectEvent(
new SimpleBindEvent(ceSearch, searchBindDNString, "searchPassword",
bindResultCode)).expectEvent(new CloseEvent(ceSearch));
// Obtain policy and state.
final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
provider);
assertTrue(factory.isConfigurationAcceptable(cfg, null));
final AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
final AuthenticationPolicyState state = policy
.createAuthenticationPolicyState(userEntry);
assertEquals(state.getAuthenticationPolicy(), policy);
// Perform authentication.
try
{
state.passwordMatches(ByteString.valueOfUtf8(userPassword));
fail("password match unexpectedly succeeded");
}
catch (final DirectoryException e)
{
// No valid connections available so this should always fail with
// INVALID_CREDENTIALS.
assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS,
e.getMessage());
}
// Tear down and check final state.
provider.assertAllExpectedEventsReceived();
state.finalizeStateAfterBind();
policy.finalizeAuthenticationPolicy();
}
/**
* Returns test data for {@link #testConnectionFailureDuringSearchBind}.
*
* @return Test data for {@link #testConnectionFailureDuringSearchBind}.
*/
@DataProvider
public Object[][] testConnectionFailureDuringSearchBindData()
{
// @formatter:off
return new Object[][] {
{ ResultCode.INVALID_CREDENTIALS },
{ ResultCode.NO_SUCH_OBJECT },
{ ResultCode.UNAVAILABLE }
};
// @formatter:on
}
/**
* Returns test data for {@link #testConnectionFailureDuringSearch}.
*
* @return Test data for {@link #testConnectionFailureDuringSearch}.
*/
@DataProvider
public Object[][] testConnectionFailureDuringSearchData()
{
// @formatter:off
return new Object[][] {
{ ResultCode.NO_SUCH_OBJECT },
{ ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED },
{ ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED },
{ ResultCode.UNAVAILABLE }
};
// @formatter:on
}
/**
* Tests that failures to obtain a search connection are handled properly.
*
* @param connectResultCode
* The connection failure result code.
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true, dataProvider = "testConnectionFailureDuringSearchGetConnectionData")
public void testConnectionFailureDuringSearchGetConnection(
final ResultCode connectResultCode) throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
.withPrimaryServer(phost1)
.withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
.withMappedAttribute("uid").withBaseDN("o=ad");
// Create the provider and its list of expected events.
final GetLDAPConnectionFactoryEvent fe = new GetLDAPConnectionFactoryEvent(
phost1, cfg);
final GetConnectionEvent ceSearch = new GetConnectionEvent(fe,
connectResultCode);
final MockProvider provider = new MockProvider().expectEvent(fe)
.expectEvent(ceSearch);
// Obtain policy and state.
final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
provider);
assertTrue(factory.isConfigurationAcceptable(cfg, null));
final AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
final AuthenticationPolicyState state = policy
.createAuthenticationPolicyState(userEntry);
assertEquals(state.getAuthenticationPolicy(), policy);
// Perform authentication.
try
{
state.passwordMatches(ByteString.valueOfUtf8(userPassword));
fail("password match unexpectedly succeeded");
}
catch (final DirectoryException e)
{
// No valid connections available so this should always fail with
// INVALID_CREDENTIALS.
assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS,
e.getMessage());
}
// Tear down and check final state.
provider.assertAllExpectedEventsReceived();
state.finalizeStateAfterBind();
policy.finalizeAuthenticationPolicy();
}
/**
* Returns test data for
* {@link #testConnectionFailureDuringSearchGetConnection}.
*
* @return Test data for
* {@link #testConnectionFailureDuringSearchGetConnection}.
*/
@DataProvider
public Object[][] testConnectionFailureDuringSearchGetConnectionData()
{
// @formatter:off
return new Object[][] {
{ ResultCode.UNAVAILABLE }
};
// @formatter:on
}
/**
* Tests fail-over between 2 primary servers then to the secondary data
* center.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testFailOverOnConnect() throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
.withPrimaryServer(phost1).withPrimaryServer(phost2)
.withSecondaryServer(shost1)
.withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
.withMappedAttribute("uid").withBaseDN("o=ad");
// Create all the events.
final MockProvider provider = new MockProvider();
// First of all the connection factories are created.
final GetLDAPConnectionFactoryEvent fe1 = new GetLDAPConnectionFactoryEvent(
phost1, cfg);
final GetLDAPConnectionFactoryEvent fe2 = new GetLDAPConnectionFactoryEvent(
phost2, cfg);
final GetLDAPConnectionFactoryEvent fe3 = new GetLDAPConnectionFactoryEvent(
shost1, cfg);
provider.expectEvent(fe1).expectEvent(fe2).expectEvent(fe3);
// Get connection for phost1, then search, then bind.
final GetConnectionEvent ceSearch1 = new GetConnectionEvent(fe1,
ResultCode.CLIENT_SIDE_CONNECT_ERROR);
final GetConnectionEvent ceSearch2 = new GetConnectionEvent(fe2,
ResultCode.CLIENT_SIDE_CONNECT_ERROR);
final GetConnectionEvent ceSearch3 = new GetConnectionEvent(fe3);
final GetConnectionEvent ceBind1 = new GetConnectionEvent(fe1,
ResultCode.CLIENT_SIDE_CONNECT_ERROR);
final GetConnectionEvent ceBind2 = new GetConnectionEvent(fe2,
ResultCode.CLIENT_SIDE_CONNECT_ERROR);
final GetConnectionEvent ceBind3 = new GetConnectionEvent(fe3);
provider
.expectEvent(ceSearch1)
.expectEvent(ceSearch2)
.expectEvent(ceSearch3)
.expectEvent(
new SimpleBindEvent(ceSearch3, searchBindDNString,
"searchPassword", ResultCode.SUCCESS))
.expectEvent(
new SearchEvent(ceSearch3, "o=ad", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", adDNString))
.expectEvent(ceBind1)
.expectEvent(ceBind2)
.expectEvent(ceBind3)
.expectEvent(
new SimpleBindEvent(ceBind3, adDNString, userPassword,
ResultCode.SUCCESS));
// Repeat again using cached connection to shost1: search, then bind.
// phost1 and phost2 will have been marked as failed and won't be tried.
provider.expectEvent(
new SearchEvent(ceSearch3, "o=ad", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", adDNString)).expectEvent(
new SimpleBindEvent(ceBind3, adDNString, userPassword,
ResultCode.SUCCESS));
// Now simulate monitor thread run in which phost2 is determined to be
// available again.
final GetConnectionEvent ceSearch2ok = new GetConnectionEvent(fe2);
final GetConnectionEvent ceBind2ok = new GetConnectionEvent(fe2);
provider
.expectEvent(ceSearch1)
.expectEvent(ceSearch2ok)
.expectEvent(
new SimpleBindEvent(ceSearch2ok, searchBindDNString,
"searchPassword", ResultCode.SUCCESS)).expectEvent(ceBind1)
.expectEvent(ceBind2ok);
provider.expectEvent(
new SearchEvent(ceSearch2ok, "o=ad", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", adDNString)).expectEvent(
new SimpleBindEvent(ceBind2ok, adDNString, userPassword,
ResultCode.SUCCESS));
// Connections should be cached until the policy is finalized.
// Obtain policy and state.
final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
provider);
assertTrue(factory.isConfigurationAcceptable(cfg, null));
final AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
// Authenticate 3 times test above fail-over.
for (int i = 0; i < 3; i++)
{
final AuthenticationPolicyState state = policy
.createAuthenticationPolicyState(userEntry);
assertEquals(state.getAuthenticationPolicy(), policy);
// Run monitor which should try to connect to phost1&2 and determine that
// phost2 is available again.
if (i == 2)
{
provider.runMonitorTasks();
}
// Perform authentication.
assertTrue(state.passwordMatches(ByteString.valueOfUtf8(userPassword)));
state.finalizeStateAfterBind();
}
// Cached connections should be closed when the policy is finalized
// (primaries first, then secondaries).
provider.expectEvent(new CloseEvent(ceSearch2ok));
provider.expectEvent(new CloseEvent(ceSearch3));
provider.expectEvent(new CloseEvent(ceBind2ok));
provider.expectEvent(new CloseEvent(ceBind3));
// Tear down and check final state.
policy.finalizeAuthenticationPolicy();
provider.assertAllExpectedEventsReceived();
}
/**
* Tests that searches which fail in one LB pool are automatically retried in
* the secondary LB pool.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testFBRetrySearchOnFailure() throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
.withPrimaryServer(phost1).withSecondaryServer(shost1)
.withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
.withMappedAttribute("uid").withBaseDN("o=ad");
// Create all the events.
final MockProvider provider = new MockProvider();
// First of all the connection factories are created.
final GetLDAPConnectionFactoryEvent fe1 = new GetLDAPConnectionFactoryEvent(
phost1, cfg);
final GetLDAPConnectionFactoryEvent fe2 = new GetLDAPConnectionFactoryEvent(
shost1, cfg);
provider.expectEvent(fe1).expectEvent(fe2);
// Get connection for phost1, then search (fail), and retry on shost1
final GetConnectionEvent ceSearch1 = new GetConnectionEvent(fe1);
final GetConnectionEvent ceSearch2 = new GetConnectionEvent(fe2);
final GetConnectionEvent ceBind1 = new GetConnectionEvent(fe1);
provider
.expectEvent(ceSearch1)
.expectEvent(
new SimpleBindEvent(ceSearch1, searchBindDNString,
"searchPassword", ResultCode.SUCCESS))
.expectEvent(
new SearchEvent(ceSearch1, "o=ad", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", ResultCode.UNAVAILABLE))
.expectEvent(new CloseEvent(ceSearch1))
.expectEvent(new GetConnectionEvent(fe1, ResultCode.UNAVAILABLE)) // pool retry
.expectEvent(ceSearch2)
.expectEvent(
new SimpleBindEvent(ceSearch2, searchBindDNString,
"searchPassword", ResultCode.SUCCESS))
.expectEvent(
new SearchEvent(ceSearch2, "o=ad", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", adDNString))
.expectEvent(ceBind1)
.expectEvent(
new SimpleBindEvent(ceBind1, adDNString, userPassword,
ResultCode.SUCCESS));
// Now simulate shost1 going down as well.
// phost1 will have been marked as failed and won't be retried.
provider
.expectEvent(
new SearchEvent(ceSearch2, "o=ad", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", ResultCode.UNAVAILABLE))
.expectEvent(new CloseEvent(ceSearch2))
.expectEvent(new GetConnectionEvent(fe2, ResultCode.UNAVAILABLE)); // pool retry
// Now simulate phost1 coming back and fail back to it.
// Now simulate monitor thread run in which phost1 and shost1 are determined
// to be available again.
provider
.expectEvent(ceSearch1)
.expectEvent(
new SimpleBindEvent(ceSearch1, searchBindDNString,
"searchPassword", ResultCode.SUCCESS))
.expectEvent(ceSearch2)
.expectEvent(
new SimpleBindEvent(ceSearch2, searchBindDNString,
"searchPassword", ResultCode.SUCCESS));
provider.expectEvent(
new SearchEvent(ceSearch1, "o=ad", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", adDNString)).expectEvent(
new SimpleBindEvent(ceBind1, adDNString, userPassword,
ResultCode.SUCCESS));
// Connections should be cached until the policy is finalized.
// Obtain policy and state.
final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
provider);
assertTrue(factory.isConfigurationAcceptable(cfg, null));
final AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
// Authenticate 3 times, second should fail.
for (int i = 0; i < 3; i++)
{
final AuthenticationPolicyState state = policy
.createAuthenticationPolicyState(userEntry);
assertEquals(state.getAuthenticationPolicy(), policy);
// Perform authentication.
switch (i)
{
case 0:
// First attempt should succeed.
assertTrue(state.passwordMatches(ByteString.valueOfUtf8(userPassword)));
break;
case 1:
// Second attempt should fail.
try
{
state.passwordMatches(ByteString.valueOfUtf8(userPassword));
fail("password match unexpectedly succeeded");
}
catch (final DirectoryException e)
{
// No valid connections available so this should always fail with
// INVALID_CREDENTIALS.
assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS,
e.getMessage());
}
break;
case 2:
// Third attempt should succeed, once the monitor has run.
provider.runMonitorTasks();
assertTrue(state.passwordMatches(ByteString.valueOfUtf8(userPassword)));
break;
}
state.finalizeStateAfterBind();
}
// Cached connections should be closed when the policy is finalized.
provider.expectEvent(new CloseEvent(ceSearch1));
provider.expectEvent(new CloseEvent(ceSearch2));
provider.expectEvent(new CloseEvent(ceBind1));
// Tear down and check final state.
policy.finalizeAuthenticationPolicy();
provider.assertAllExpectedEventsReceived();
}
/**
* Tests configuration validation.
*
* @param cfg
* The configuration to be tested.
* @param isValid
* Whether or not the provided configuration is valid.
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true, dataProvider = "testIsConfigurationAcceptableData")
public void testIsConfigurationAcceptable(
final LDAPPassThroughAuthenticationPolicyCfg cfg, final boolean isValid)
throws Exception
{
final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory();
assertEquals(
factory.isConfigurationAcceptable(cfg, new LinkedList<LocalizableMessage>()),
isValid);
}
/**
* Returns test data for {@link #testIsConfigurationAcceptable}.
*
* @return Test data for {@link #testIsConfigurationAcceptable}.
*/
@DataProvider
public Object[][] testIsConfigurationAcceptableData()
{
// @formatter:off
return new Object[][] {
/* cfg, isValid */
// Test server configuration.
{ mockCfg().withPrimaryServer("test:1"), true },
{ mockCfg().withPrimaryServer("test:65535"), true },
{ mockCfg().withPrimaryServer("test:0"), false },
{ mockCfg().withPrimaryServer("test:65536"), false },
{ mockCfg().withPrimaryServer("test:1000000"), false },
{ mockCfg().withSecondaryServer("test:1"), true },
{ mockCfg().withSecondaryServer("test:65535"), true },
{ mockCfg().withSecondaryServer("test:0"), false },
{ mockCfg().withSecondaryServer("test:65536"), false },
{ mockCfg().withSecondaryServer("test:1000000"), false },
// Test mapped search parameters.
{ mockCfg().withMappingPolicy(MappingPolicy.MAPPED_SEARCH), true },
{ mockCfg().withMappingPolicy(MappingPolicy.MAPPED_SEARCH).withMappedSearchBindDN(null).withMappedSearchBindPassword(null), true },
{ mockCfg().withMappingPolicy(MappingPolicy.MAPPED_SEARCH).withMappedSearchBindPassword(null), false },
{ mockCfg().withMappingPolicy(MappingPolicy.MAPPED_SEARCH).withMappedSearchBindPasswordProperty("org.opendj.dummy.property"), false },
{ mockCfg().withMappingPolicy(MappingPolicy.MAPPED_SEARCH).withMappedSearchBindPasswordProperty("java.version"), true },
{ mockCfg().withMappingPolicy(MappingPolicy.MAPPED_SEARCH).withMappedSearchBindPasswordEnvironmentVariable("ORG_OPENDJ_DUMMY_ENVVAR"), false },
{ mockCfg().withMappingPolicy(MappingPolicy.MAPPED_SEARCH).withMappedSearchBindPasswordFile("dummy_file.txt"), false },
{ mockCfg().withMappingPolicy(MappingPolicy.MAPPED_SEARCH).withMappedSearchBindPasswordFile("config/admin-keystore.pin"), true },
};
// @formatter:on
}
/**
* Tests that searches which fail on one server are automatically retried on
* another within the same LB.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testLBRetrySearchOnFailure() throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
.withPrimaryServer(phost1).withPrimaryServer(phost2)
.withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
.withMappedAttribute("uid").withBaseDN("o=ad");
// Create all the events.
final MockProvider provider = new MockProvider();
// First of all the connection factories are created.
final GetLDAPConnectionFactoryEvent fe1 = new GetLDAPConnectionFactoryEvent(
phost1, cfg);
final GetLDAPConnectionFactoryEvent fe2 = new GetLDAPConnectionFactoryEvent(
phost2, cfg);
provider.expectEvent(fe1).expectEvent(fe2);
// Get connection for phost1, then search (fail), and retry on phost2
final GetConnectionEvent ceSearch1 = new GetConnectionEvent(fe1);
final GetConnectionEvent ceSearch2 = new GetConnectionEvent(fe2);
final GetConnectionEvent ceBind1 = new GetConnectionEvent(fe1);
final GetConnectionEvent ceBind2 = new GetConnectionEvent(fe2);
provider
.expectEvent(ceSearch1)
.expectEvent(
new SimpleBindEvent(ceSearch1, searchBindDNString,
"searchPassword", ResultCode.SUCCESS))
.expectEvent(
new SearchEvent(ceSearch1, "o=ad", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", ResultCode.UNAVAILABLE))
.expectEvent(new CloseEvent(ceSearch1))
.expectEvent(new GetConnectionEvent(fe1, ResultCode.UNAVAILABLE)) // pool retry
.expectEvent(ceSearch2)
.expectEvent(
new SimpleBindEvent(ceSearch2, searchBindDNString,
"searchPassword", ResultCode.SUCCESS))
.expectEvent(
new SearchEvent(ceSearch2, "o=ad", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", adDNString))
.expectEvent(ceBind1)
.expectEvent(
new SimpleBindEvent(ceBind1, adDNString, userPassword,
ResultCode.SUCCESS));
// Now simulate phost2 going down as well.
// phost1 will have been marked as failed and won't be retried.
provider.expectEvent(
new SearchEvent(ceSearch2, "o=ad", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", ResultCode.UNAVAILABLE)).expectEvent(
new CloseEvent(ceSearch2))
.expectEvent(new GetConnectionEvent(fe2, ResultCode.UNAVAILABLE)); // pool retry
// Now simulate phost1 coming back and fail back to it.
// Now simulate monitor thread run in which phost1 and shost1 are determined
// to be available again.
provider
.expectEvent(ceSearch1)
.expectEvent(
new SimpleBindEvent(ceSearch1, searchBindDNString,
"searchPassword", ResultCode.SUCCESS))
.expectEvent(ceSearch2)
.expectEvent(
new SimpleBindEvent(ceSearch2, searchBindDNString,
"searchPassword", ResultCode.SUCCESS));
// Note that the bind will be load-balanced.
provider
.expectEvent(
new SearchEvent(ceSearch1, "o=ad", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", adDNString))
.expectEvent(ceBind2)
.expectEvent(
new SimpleBindEvent(ceBind2, adDNString, userPassword,
ResultCode.SUCCESS));
// Connections should be cached until the policy is finalized.
// Obtain policy and state.
final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
provider);
assertTrue(factory.isConfigurationAcceptable(cfg, null));
final AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
// Authenticate 3 times, second should fail.
for (int i = 0; i < 3; i++)
{
final AuthenticationPolicyState state = policy
.createAuthenticationPolicyState(userEntry);
assertEquals(state.getAuthenticationPolicy(), policy);
// Perform authentication.
switch (i)
{
case 0:
// First attempt should succeed.
assertTrue(state.passwordMatches(ByteString.valueOfUtf8(userPassword)));
break;
case 1:
// Second attempt should fail.
try
{
state.passwordMatches(ByteString.valueOfUtf8(userPassword));
fail("password match unexpectedly succeeded");
}
catch (final DirectoryException e)
{
// No valid connections available so this should always fail with
// INVALID_CREDENTIALS.
assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS,
e.getMessage());
}
break;
case 2:
// Third attempt should succeed, once the monitor has run.
provider.runMonitorTasks();
assertTrue(state.passwordMatches(ByteString.valueOfUtf8(userPassword)));
break;
}
state.finalizeStateAfterBind();
}
// Cached connections should be closed when the policy is finalized.
provider.expectEvent(new CloseEvent(ceSearch1));
provider.expectEvent(new CloseEvent(ceSearch2));
provider.expectEvent(new CloseEvent(ceBind1));
provider.expectEvent(new CloseEvent(ceBind2));
// Tear down and check final state.
policy.finalizeAuthenticationPolicy();
provider.assertAllExpectedEventsReceived();
}
/**
* Tests valid bind which times out at the client. These should trigger a
* CLIENT_SIDE_TIMEOUT result code.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testLDAPConnectionFactoryBindClientTimeout() throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
.withConnectionTimeout(500);
// Mock server.
final MockServer server = mockServer().thenAccept().thenBlock().start();
// Test connect and close.
final LDAPConnectionFactory factory = new LDAPConnectionFactory(
"127.0.0.1", server.getPort(), cfg);
Connection connection = null;
try
{
connection = factory.getConnection();
connection.simpleBind(ByteString.valueOfUtf8(searchBindDNString),
ByteString.valueOfUtf8(userPassword));
fail("Bind attempt should have failed");
}
catch (final DirectoryException e)
{
assertEquals(e.getResultCode(), ResultCode.CLIENT_SIDE_TIMEOUT,
e.getMessage());
}
finally
{
StaticUtils.close(connection);
server.unblock();
server.stop();
}
}
/**
* Tests valid bind which never receives a response because the server
* abruptly closes the connection.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testLDAPConnectionFactoryBindConnectionClosed() throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
// Mock server.
final MockServer server = mockServer().thenAccept()
.thenReceive(1, newBindRequest(searchBindDNString, userPassword))
.thenClose().start();
// Test connect and close.
final LDAPConnectionFactory factory = new LDAPConnectionFactory(
"127.0.0.1", server.getPort(), cfg);
Connection connection = null;
try
{
connection = factory.getConnection();
connection.simpleBind(ByteString.valueOfUtf8(searchBindDNString),
ByteString.valueOfUtf8(userPassword));
fail("Bind attempt should have failed");
}
catch (final DirectoryException e)
{
assertEquals(e.getResultCode(), ResultCode.CLIENT_SIDE_SERVER_DOWN,
e.getMessage());
}
finally
{
StaticUtils.close(connection);
server.stop();
}
}
/**
* Tests bind which receives a disconnect notification. The error result code
* should be passed back to the called.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testLDAPConnectionFactoryBindDisconnectNotification()
throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
// Mock server.
final MockServer server = mockServer()
.thenAccept()
.thenReceive(1, newBindRequest(searchBindDNString, userPassword))
.thenSend(0 /* disconnect ID */,
newDisconnectNotification(ResultCode.UNAVAILABLE)).start();
// Test connect and close.
final LDAPConnectionFactory factory = new LDAPConnectionFactory(
"127.0.0.1", server.getPort(), cfg);
Connection connection = null;
try
{
connection = factory.getConnection();
connection.simpleBind(ByteString.valueOfUtf8(searchBindDNString),
ByteString.valueOfUtf8(userPassword));
fail("Bind attempt should have failed");
}
catch (final DirectoryException e)
{
// Should be the result code sent by the server.
assertEquals(e.getResultCode(), ResultCode.UNAVAILABLE, e.getMessage());
}
finally
{
StaticUtils.close(connection);
server.stop();
}
}
/**
* Tests bind with invalid credentials which should return a
* INVALID_CREDENTIALS result code.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testLDAPConnectionFactoryBindInvalidCredentials()
throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
// Mock server.
final MockServer server = mockServer().thenAccept()
.thenReceive(1, newBindRequest(searchBindDNString, userPassword))
.thenSend(1, newBindResult(ResultCode.INVALID_CREDENTIALS)).start();
// Test connect and close.
final LDAPConnectionFactory factory = new LDAPConnectionFactory(
"127.0.0.1", server.getPort(), cfg);
Connection connection = null;
try
{
connection = factory.getConnection();
connection.simpleBind(ByteString.valueOfUtf8(searchBindDNString),
ByteString.valueOfUtf8(userPassword));
fail("Bind attempt should have failed");
}
catch (final DirectoryException e)
{
assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS,
e.getMessage());
}
finally
{
StaticUtils.close(connection);
server.stop();
}
}
/**
* Tests bind which returns an error result. The error result code should be
* passed back to the caller.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testLDAPConnectionFactoryBindOtherError() throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
// Mock server.
final MockServer server = mockServer().thenAccept()
.thenReceive(1, newBindRequest(searchBindDNString, userPassword))
.thenSend(1, newBindResult(ResultCode.UNAVAILABLE)).start();
// Test connect and close.
final LDAPConnectionFactory factory = new LDAPConnectionFactory(
"127.0.0.1", server.getPort(), cfg);
Connection connection = null;
try
{
connection = factory.getConnection();
connection.simpleBind(ByteString.valueOfUtf8(searchBindDNString),
ByteString.valueOfUtf8(userPassword));
fail("Bind attempt should have failed");
}
catch (final DirectoryException e)
{
assertEquals(e.getResultCode(), ResultCode.UNAVAILABLE, e.getMessage());
}
finally
{
StaticUtils.close(connection);
server.stop();
}
}
/**
* Tests valid bind returning success.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testLDAPConnectionFactoryBindSuccess() throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
// Mock server.
final MockServer server = mockServer().thenAccept()
.thenReceive(1, newBindRequest(searchBindDNString, userPassword))
.thenSend(1, newBindResult(ResultCode.SUCCESS)).start();
// Test connect and close.
final LDAPConnectionFactory factory = new LDAPConnectionFactory(
"127.0.0.1", server.getPort(), cfg);
Connection connection = null;
try
{
connection = factory.getConnection();
connection.simpleBind(ByteString.valueOfUtf8(searchBindDNString),
ByteString.valueOfUtf8(userPassword));
}
finally
{
StaticUtils.close(connection);
server.stop();
}
}
/**
* Tests successful connect/unbind.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testLDAPConnectionFactoryConnectAndUnbind() throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
// Mock server.
final MockServer server = mockServer().thenAccept()
.thenReceive(1, new UnbindRequestProtocolOp()).thenClose().start();
// Test connect and close.
final LDAPConnectionFactory factory = new LDAPConnectionFactory(
"127.0.0.1", server.getPort(), cfg);
try
{
final Connection connection = factory.getConnection();
connection.close();
}
finally
{
server.stop();
}
}
/**
* Tests that invalid ports are handled properly. These should trigger a
* CLIENT_SIDE_CONNECT_ERROR result code.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testLDAPConnectionFactoryConnectPortNotInUse() throws Exception
{
final int port = TestCaseUtils.findFreePort();
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
// Test connect and close.
final LDAPConnectionFactory factory = new LDAPConnectionFactory(
"127.0.0.1", port, cfg);
Connection connection = null;
try
{
connection = factory.getConnection();
fail("Connect attempt should have failed");
}
catch (final DirectoryException e)
{
assertEquals(e.getResultCode(), ResultCode.CLIENT_SIDE_CONNECT_ERROR,
e.getMessage());
}
finally
{
StaticUtils.close(connection);
}
}
/**
* Tests that unknown hosts are handled properly. These should trigger a
* CLIENT_SIDE_CONNECT_ERROR result code.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testLDAPConnectionFactoryConnectUnknownHost() throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
// FIXME: can we guarantee that "unknownhost" does not exist?
final LDAPConnectionFactory factory = new LDAPConnectionFactory(
"unknownhost", 31415, cfg);
Connection connection = null;
try
{
connection = factory.getConnection();
fail("Connect attempt should have failed");
}
catch (final DirectoryException e)
{
assertEquals(e.getResultCode(), ResultCode.CLIENT_SIDE_CONNECT_ERROR,
e.getMessage());
}
finally
{
StaticUtils.close(connection);
}
}
/**
* Tests valid search which times out at the client. These should trigger a
* CLIENT_SIDE_TIMEOUT result code.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testLDAPConnectionFactorySearchClientTimeout() throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
.withConnectionTimeout(500);
// Mock server.
final MockServer server = mockServer().thenAccept().thenBlock().start();
// Test connect and close.
final LDAPConnectionFactory factory = new LDAPConnectionFactory(
"127.0.0.1", server.getPort(), cfg);
Connection connection = null;
try
{
connection = factory.getConnection();
connection.search(searchBindDN, SearchScope.WHOLE_SUBTREE, SearchFilter.objectClassPresent());
fail("Search attempt should have timed out");
}
catch (final DirectoryException e)
{
assertEquals(e.getResultCode(), ResultCode.CLIENT_SIDE_TIMEOUT,
e.getMessage());
}
finally
{
StaticUtils.close(connection);
server.unblock();
server.stop();
}
}
/**
* Tests valid search which never receives a response because the server
* abruptly closes the connection.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testLDAPConnectionFactorySearchConnectionClosed()
throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
// Mock server.
final MockServer server = mockServer()
.thenAccept()
.thenReceive(1,
newSearchRequest(searchBindDNString, "(uid=aduser)", cfg))
.thenClose().start();
// Test connect and close.
final LDAPConnectionFactory factory = new LDAPConnectionFactory(
"127.0.0.1", server.getPort(), cfg);
Connection connection = null;
try
{
connection = factory.getConnection();
connection.search(searchBindDN, SearchScope.WHOLE_SUBTREE,
SearchFilter.createFilterFromString("(uid=aduser)"));
fail("Search attempt should have failed");
}
catch (final DirectoryException e)
{
assertEquals(e.getResultCode(), ResultCode.CLIENT_SIDE_SERVER_DOWN,
e.getMessage());
}
finally
{
StaticUtils.close(connection);
server.stop();
}
}
/**
* Tests valid search which receives a disconnect notification. The error
* result code should be passed back to the called.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testLDAPConnectionFactorySearchDisconnectNotification()
throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
// Mock server.
final MockServer server = mockServer()
.thenAccept()
.thenReceive(1,
newSearchRequest(searchBindDNString, "(uid=aduser)", cfg))
.thenSend(0 /* disconnect ID */,
newDisconnectNotification(ResultCode.UNAVAILABLE)).start();
// Test connect and close.
final LDAPConnectionFactory factory = new LDAPConnectionFactory(
"127.0.0.1", server.getPort(), cfg);
Connection connection = null;
try
{
connection = factory.getConnection();
connection.search(searchBindDN, SearchScope.WHOLE_SUBTREE,
SearchFilter.createFilterFromString("(uid=aduser)"));
fail("Search attempt should have failed");
}
catch (final DirectoryException e)
{
// Should be the result code sent by the server.
assertEquals(e.getResultCode(), ResultCode.UNAVAILABLE, e.getMessage());
}
finally
{
StaticUtils.close(connection);
server.stop();
}
}
/**
* Tests valid search returning no results are handled properly. These should
* trigger a CLIENT_SIDE_NO_RESULTS_RETURNED result code.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testLDAPConnectionFactorySearchNoResults() throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
// Mock server.
final MockServer server = mockServer()
.thenAccept()
.thenReceive(1,
newSearchRequest(searchBindDNString, "(uid=aduser)", cfg))
.thenSend(1, newSearchResult(ResultCode.SUCCESS)).start();
// Test connect and close.
final LDAPConnectionFactory factory = new LDAPConnectionFactory(
"127.0.0.1", server.getPort(), cfg);
Connection connection = null;
try
{
connection = factory.getConnection();
connection.search(searchBindDN, SearchScope.WHOLE_SUBTREE,
SearchFilter.createFilterFromString("(uid=aduser)"));
fail("Search attempt should have failed");
}
catch (final DirectoryException e)
{
assertEquals(e.getResultCode(),
ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED, e.getMessage());
}
finally
{
StaticUtils.close(connection);
server.stop();
}
}
/**
* Tests search returning no entries and an error result. The error result
* code should be passed back to the caller.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testLDAPConnectionFactorySearchOtherError() throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
// Mock server.
final MockServer server = mockServer()
.thenAccept()
.thenReceive(1,
newSearchRequest(searchBindDNString, "(uid=aduser)", cfg))
.thenSend(1, newSearchResult(ResultCode.UNAVAILABLE)).start();
// Test connect and close.
final LDAPConnectionFactory factory = new LDAPConnectionFactory(
"127.0.0.1", server.getPort(), cfg);
Connection connection = null;
try
{
connection = factory.getConnection();
connection.search(searchBindDN, SearchScope.WHOLE_SUBTREE,
SearchFilter.createFilterFromString("(uid=aduser)"));
fail("Search attempt should have failed");
}
catch (final DirectoryException e)
{
// Should be the result code sent by the server.
assertEquals(e.getResultCode(), ResultCode.UNAVAILABLE, e.getMessage());
}
finally
{
StaticUtils.close(connection);
server.stop();
}
}
/**
* Tests valid search returning a single entry followed by a size limit
* exceeded error are handled properly. These should trigger a
* CLIENT_SIDE_MORE_RESULTS_TO_RETURN result code.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testLDAPConnectionFactorySearchSizeLimit() throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
// Mock server.
final MockServer server = mockServer()
.thenAccept()
.thenReceive(1,
newSearchRequest(searchBindDNString, "(uid=aduser)", cfg))
.thenSend(1, newSearchEntry(adDNString))
.thenSend(1, newSearchResult(ResultCode.SIZE_LIMIT_EXCEEDED)).start();
// Test connect and close.
final LDAPConnectionFactory factory = new LDAPConnectionFactory(
"127.0.0.1", server.getPort(), cfg);
Connection connection = null;
try
{
connection = factory.getConnection();
connection.search(searchBindDN, SearchScope.WHOLE_SUBTREE,
SearchFilter.createFilterFromString("(uid=aduser)"));
fail("Search attempt should have failed");
}
catch (final DirectoryException e)
{
assertEquals(e.getResultCode(),
ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED, e.getMessage());
}
finally
{
StaticUtils.close(connection);
server.stop();
}
}
/**
* Tests valid search returning a single entry works properly.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testLDAPConnectionFactorySearchSuccess() throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
// Mock server.
final MockServer server = mockServer()
.thenAccept()
.thenReceive(1,
newSearchRequest(searchBindDNString, "(uid=aduser)", cfg))
.thenSend(1, newSearchEntry(adDNString))
.thenSend(1, newSearchResult(ResultCode.SUCCESS)).start();
// Test connect and close.
final LDAPConnectionFactory factory = new LDAPConnectionFactory(
"127.0.0.1", server.getPort(), cfg);
Connection connection = null;
try
{
connection = factory.getConnection();
final ByteString username = connection.search(searchBindDN,
SearchScope.WHOLE_SUBTREE,
SearchFilter.createFilterFromString("(uid=aduser)"));
assertEquals(username, ByteString.valueOfUtf8(adDNString));
}
finally
{
StaticUtils.close(connection);
server.stop();
}
}
/**
* Tests valid search returning a single entry followed by a time limit
* exceeded error are handled properly. These should trigger a
* CLIENT_SIDE_MORE_RESULTS_TO_RETURN result code.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testLDAPConnectionFactorySearchTimeLimit() throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
// Mock server.
final MockServer server = mockServer()
.thenAccept()
.thenReceive(1,
newSearchRequest(searchBindDNString, "(uid=aduser)", cfg))
.thenSend(1, newSearchEntry(adDNString))
.thenSend(1, newSearchResult(ResultCode.TIME_LIMIT_EXCEEDED)).start();
// Test connect and close.
final LDAPConnectionFactory factory = new LDAPConnectionFactory(
"127.0.0.1", server.getPort(), cfg);
Connection connection = null;
try
{
connection = factory.getConnection();
connection.search(searchBindDN, SearchScope.WHOLE_SUBTREE,
SearchFilter.createFilterFromString("(uid=aduser)"));
fail("Search attempt should have failed");
}
catch (final DirectoryException e)
{
assertEquals(e.getResultCode(), ResultCode.TIME_LIMIT_EXCEEDED,
e.getMessage());
}
finally
{
StaticUtils.close(connection);
server.stop();
}
}
/**
* Tests valid search returning many results are handled properly. These
* should trigger a CLIENT_SIDE_MORE_RESULTS_TO_RETURN result code.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testLDAPConnectionFactorySearchTooManyResults() throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
// Mock server.
final MockServer server = mockServer()
.thenAccept()
.thenReceive(1,
newSearchRequest(searchBindDNString, "(uid=aduser)", cfg))
.thenSend(1, newSearchEntry(adDNString))
.thenSend(1, newSearchEntry(opendjDNString))
.thenSend(1, newSearchResult(ResultCode.SUCCESS)).start();
// Test connect and close.
final LDAPConnectionFactory factory = new LDAPConnectionFactory(
"127.0.0.1", server.getPort(), cfg);
Connection connection = null;
try
{
connection = factory.getConnection();
connection.search(searchBindDN, SearchScope.WHOLE_SUBTREE,
SearchFilter.createFilterFromString("(uid=aduser)"));
fail("Search attempt should have failed");
}
catch (final DirectoryException e)
{
assertEquals(e.getResultCode(),
ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED, e.getMessage());
}
finally
{
StaticUtils.close(connection);
server.stop();
}
}
/**
* Tests load balancing across 3 servers.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testLoadBalancing() throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
.withPrimaryServer(phost1).withPrimaryServer(phost2)
.withPrimaryServer(phost3)
.withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
.withMappedAttribute("uid").withBaseDN("o=ad");
// Create all the events.
final MockProvider provider = new MockProvider();
// First of all the connection factories are created.
final GetLDAPConnectionFactoryEvent fe1 = new GetLDAPConnectionFactoryEvent(
phost1, cfg);
final GetLDAPConnectionFactoryEvent fe2 = new GetLDAPConnectionFactoryEvent(
phost2, cfg);
final GetLDAPConnectionFactoryEvent fe3 = new GetLDAPConnectionFactoryEvent(
phost3, cfg);
provider.expectEvent(fe1).expectEvent(fe2).expectEvent(fe3);
// Get connection for phost1, then search, then bind.
final GetConnectionEvent ceSearch1 = new GetConnectionEvent(fe1);
final GetConnectionEvent ceBind1 = new GetConnectionEvent(fe1);
provider
.expectEvent(ceSearch1)
.expectEvent(
new SimpleBindEvent(ceSearch1, searchBindDNString,
"searchPassword", ResultCode.SUCCESS))
.expectEvent(
new SearchEvent(ceSearch1, "o=ad", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", adDNString))
.expectEvent(ceBind1)
.expectEvent(
new SimpleBindEvent(ceBind1, adDNString, userPassword,
ResultCode.SUCCESS));
// Get connection for phost2, then search, then bind.
final GetConnectionEvent ceSearch2 = new GetConnectionEvent(fe2);
final GetConnectionEvent ceBind2 = new GetConnectionEvent(fe2);
provider
.expectEvent(ceSearch2)
.expectEvent(
new SimpleBindEvent(ceSearch2, searchBindDNString,
"searchPassword", ResultCode.SUCCESS))
.expectEvent(
new SearchEvent(ceSearch2, "o=ad", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", adDNString))
.expectEvent(ceBind2)
.expectEvent(
new SimpleBindEvent(ceBind2, adDNString, userPassword,
ResultCode.SUCCESS));
// Get connection for phost3, then search, then bind.
final GetConnectionEvent ceSearch3 = new GetConnectionEvent(fe3);
final GetConnectionEvent ceBind3 = new GetConnectionEvent(fe3);
provider
.expectEvent(ceSearch3)
.expectEvent(
new SimpleBindEvent(ceSearch3, searchBindDNString,
"searchPassword", ResultCode.SUCCESS))
.expectEvent(
new SearchEvent(ceSearch3, "o=ad", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", adDNString))
.expectEvent(ceBind3)
.expectEvent(
new SimpleBindEvent(ceBind3, adDNString, userPassword,
ResultCode.SUCCESS));
// Repeat again using cached connection to phost1: search, then bind.
provider.expectEvent(
new SearchEvent(ceSearch1, "o=ad", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", adDNString)).expectEvent(
new SimpleBindEvent(ceBind1, adDNString, userPassword,
ResultCode.SUCCESS));
// Repeat again using cached connection to phost2: search, then bind.
provider.expectEvent(
new SearchEvent(ceSearch2, "o=ad", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", adDNString)).expectEvent(
new SimpleBindEvent(ceBind2, adDNString, userPassword,
ResultCode.SUCCESS));
// Repeat again using cached connection to phost3: search, then bind.
provider.expectEvent(
new SearchEvent(ceSearch3, "o=ad", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", adDNString)).expectEvent(
new SimpleBindEvent(ceBind3, adDNString, userPassword,
ResultCode.SUCCESS));
// Connections should be cached until the policy is finalized.
// Obtain policy and state.
final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
provider);
assertTrue(factory.isConfigurationAcceptable(cfg, null));
final AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
// Cycle twice through the LB pool.
for (int i = 0; i < 6; i++)
{
final AuthenticationPolicyState state = policy
.createAuthenticationPolicyState(userEntry);
assertEquals(state.getAuthenticationPolicy(), policy);
// Perform authentication.
assertTrue(state.passwordMatches(ByteString.valueOfUtf8(userPassword)));
state.finalizeStateAfterBind();
}
// Cached connections should be closed when the policy is finalized.
provider.expectEvent(new CloseEvent(ceSearch1));
provider.expectEvent(new CloseEvent(ceSearch2));
provider.expectEvent(new CloseEvent(ceSearch3));
provider.expectEvent(new CloseEvent(ceBind1));
provider.expectEvent(new CloseEvent(ceBind2));
provider.expectEvent(new CloseEvent(ceBind3));
// Tear down and check final state.
policy.finalizeAuthenticationPolicy();
provider.assertAllExpectedEventsReceived();
}
/**
* Tests the different mapping policies: connection attempts will succeed, as
* will any searches, but the final user bind may or may not succeed depending
* on the provided result code.
* <p>
* Non-fatal errors (e.g. entry not found) should not cause the bind
* connection to be closed.
*
* @param mappingPolicy
* The mapping policy.
* @param bindResultCode
* The bind result code.
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true, dataProvider = "testMappingPolicyAuthenticationData")
public void testMappingPolicyAuthentication(
final MappingPolicy mappingPolicy, final ResultCode bindResultCode)
throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
.withPrimaryServer(phost1)
.withMappingPolicy(mappingPolicy)
.withMappedAttribute(
mappingPolicy == MappingPolicy.MAPPED_BIND ? "aduser" : "uid")
.withBaseDN("o=ad");
// Create the provider and its list of expected events.
final GetLDAPConnectionFactoryEvent fe = new GetLDAPConnectionFactoryEvent(
phost1, cfg);
final MockProvider provider = new MockProvider().expectEvent(fe);
// Add search events if doing a mapped search.
GetConnectionEvent ceSearch = null;
if (mappingPolicy == MappingPolicy.MAPPED_SEARCH)
{
ceSearch = new GetConnectionEvent(fe);
provider
.expectEvent(ceSearch)
.expectEvent(
new SimpleBindEvent(ceSearch, searchBindDNString,
"searchPassword", ResultCode.SUCCESS))
.expectEvent(
new SearchEvent(ceSearch, "o=ad", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", adDNString));
// Connection should be cached until the policy is finalized.
}
// Add bind events.
final GetConnectionEvent ceBind = new GetConnectionEvent(fe);
provider.expectEvent(ceBind).expectEvent(
new SimpleBindEvent(ceBind,
mappingPolicy == MappingPolicy.UNMAPPED ? opendjDNString
: adDNString, userPassword, bindResultCode));
if (isServiceError(bindResultCode))
{
// The connection will fail and be closed immediately, and the pool will
// retry on new connection.
provider.expectEvent(new CloseEvent(ceBind));
provider.expectEvent(new GetConnectionEvent(fe, bindResultCode));
}
// Connection should be cached until the policy is finalized or until the
// connection fails.
// Obtain policy and state.
final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
provider);
assertTrue(factory.isConfigurationAcceptable(cfg, null));
final AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
final AuthenticationPolicyState state = policy
.createAuthenticationPolicyState(userEntry);
assertEquals(state.getAuthenticationPolicy(), policy);
// Perform authentication.
switch (bindResultCode.asEnum())
{
case SUCCESS:
assertTrue(state.passwordMatches(ByteString.valueOfUtf8(userPassword)));
break;
case INVALID_CREDENTIALS:
assertFalse(state.passwordMatches(ByteString.valueOfUtf8(userPassword)));
break;
default:
try
{
state.passwordMatches(ByteString.valueOfUtf8(userPassword));
fail("password match did not fail");
}
catch (final DirectoryException e)
{
// No valid connections available so this should always fail with
// INVALID_CREDENTIALS.
assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS,
e.getMessage());
}
break;
}
// There should be no more pending events.
provider.assertAllExpectedEventsReceived();
state.finalizeStateAfterBind();
// Cached connections should be closed when the policy is finalized.
if (ceSearch != null)
{
provider.expectEvent(new CloseEvent(ceSearch));
}
if (!isServiceError(bindResultCode))
{
provider.expectEvent(new CloseEvent(ceBind));
}
// Tear down and check final state.
policy.finalizeAuthenticationPolicy();
provider.assertAllExpectedEventsReceived();
}
/**
* Returns test data for {@link #testMappingPolicyAuthentication}.
*
* @return Test data for {@link #testMappingPolicyAuthentication}.
*/
@DataProvider
public Object[][] testMappingPolicyAuthenticationData()
{
// @formatter:off
return new Object[][] {
/* policy, bind result code */
{ MappingPolicy.UNMAPPED, ResultCode.SUCCESS },
{ MappingPolicy.UNMAPPED, ResultCode.INVALID_CREDENTIALS },
{ MappingPolicy.UNMAPPED, ResultCode.UNAVAILABLE },
{ MappingPolicy.MAPPED_BIND, ResultCode.SUCCESS },
{ MappingPolicy.MAPPED_BIND, ResultCode.INVALID_CREDENTIALS },
{ MappingPolicy.MAPPED_BIND, ResultCode.UNAVAILABLE },
{ MappingPolicy.MAPPED_SEARCH, ResultCode.SUCCESS },
{ MappingPolicy.MAPPED_SEARCH, ResultCode.INVALID_CREDENTIALS },
{ MappingPolicy.MAPPED_SEARCH, ResultCode.UNAVAILABLE },
};
// @formatter:on
}
/**
* Tests that mapped PTA fails when no match attribute values are found.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testMissingMappingAttributes() throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
.withPrimaryServer(phost1)
.withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
.withMappedAttribute("uid1").withBaseDN("o=ad");
// Create the provider and its list of expected events.
final GetLDAPConnectionFactoryEvent fe = new GetLDAPConnectionFactoryEvent(
phost1, cfg);
final MockProvider provider = new MockProvider().expectEvent(fe);
// Obtain policy and state.
final Entry testUser = TestCaseUtils.makeEntry(
/* @formatter:off */
"dn: " + opendjDNString,
"objectClass: top",
"objectClass: person",
"sn: user",
"cn: test user",
"aduser: " + adDNString
/* @formatter:on */
);
final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
provider);
assertTrue(factory.isConfigurationAcceptable(cfg, null));
final AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
final AuthenticationPolicyState state = policy
.createAuthenticationPolicyState(testUser);
assertEquals(state.getAuthenticationPolicy(), policy);
// Perform authentication.
try
{
state.passwordMatches(ByteString.valueOfUtf8(userPassword));
fail("password match unexpectedly succeeded");
}
catch (final DirectoryException e)
{
// No mapping attributes so this should always fail with
// INVALID_CREDENTIALS.
assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS,
e.getMessage());
}
// There should be no more pending events.
provider.assertAllExpectedEventsReceived();
state.finalizeStateAfterBind();
// Tear down and check final state.
policy.finalizeAuthenticationPolicy();
provider.assertAllExpectedEventsReceived();
}
/**
* Tests that mapped PTA uses an appropriate filter when multiple match
* attributes are defined.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testMultipleMappingAttributes() throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
.withPrimaryServer(phost1)
.withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
.withMappedAttribute("uid1").withMappedAttribute("uid2")
.withMappedAttribute("uid3").withBaseDN("o=ad");
// Create the provider and its list of expected events.
final GetLDAPConnectionFactoryEvent fe = new GetLDAPConnectionFactoryEvent(
phost1, cfg);
final MockProvider provider = new MockProvider().expectEvent(fe);
// Add search events.
final GetConnectionEvent ceSearch = new GetConnectionEvent(fe);
provider
.expectEvent(ceSearch)
.expectEvent(
new SimpleBindEvent(ceSearch, searchBindDNString, "searchPassword",
ResultCode.SUCCESS))
.expectEvent(
new SearchEvent(ceSearch, "o=ad", SearchScope.WHOLE_SUBTREE,
"(|(uid1=one)(uid2=two)(uid3=three))", adDNString));
// Connection should be cached until the policy is finalized.
// Add bind events.
final GetConnectionEvent ceBind = new GetConnectionEvent(fe);
provider.expectEvent(ceBind).expectEvent(
new SimpleBindEvent(ceBind, adDNString, userPassword,
ResultCode.SUCCESS));
// Connection should be cached until the policy is finalized.
// Obtain policy and state.
final Entry testUser = TestCaseUtils.makeEntry(
/* @formatter:off */
"dn: " + opendjDNString,
"objectClass: top",
"objectClass: person",
"sn: user",
"cn: test user",
"aduser: " + adDNString,
"uid1: one",
"uid2: two",
"uid3: three"
/* @formatter:on */
);
final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
provider);
assertTrue(factory.isConfigurationAcceptable(cfg, null));
final AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
final AuthenticationPolicyState state = policy
.createAuthenticationPolicyState(testUser);
assertEquals(state.getAuthenticationPolicy(), policy);
// Perform authentication.
assertTrue(state.passwordMatches(ByteString.valueOfUtf8(userPassword)));
// There should be no more pending events.
provider.assertAllExpectedEventsReceived();
state.finalizeStateAfterBind();
// Cached connections should be closed when the policy is finalized.
provider.expectEvent(new CloseEvent(ceSearch));
provider.expectEvent(new CloseEvent(ceBind));
// Tear down and check final state.
policy.finalizeAuthenticationPolicy();
provider.assertAllExpectedEventsReceived();
}
/**
* Tests that mapped PTA uses an appropriate filter when multiple match
* attribute values are found.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testMultipleMappingAttributeValues() throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
.withPrimaryServer(phost1)
.withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
.withMappedAttribute("uid1").withBaseDN("o=ad");
// Create the provider and its list of expected events.
final GetLDAPConnectionFactoryEvent fe = new GetLDAPConnectionFactoryEvent(
phost1, cfg);
final MockProvider provider = new MockProvider().expectEvent(fe);
// Add search events.
final GetConnectionEvent ceSearch = new GetConnectionEvent(fe);
provider
.expectEvent(ceSearch)
.expectEvent(
new SimpleBindEvent(ceSearch, searchBindDNString, "searchPassword",
ResultCode.SUCCESS))
.expectEvent(
new SearchEvent(ceSearch, "o=ad", SearchScope.WHOLE_SUBTREE,
"(|(uid1=one)(uid1=two)(uid1=three))", adDNString));
// Connection should be cached until the policy is finalized.
// Add bind events.
final GetConnectionEvent ceBind = new GetConnectionEvent(fe);
provider.expectEvent(ceBind).expectEvent(
new SimpleBindEvent(ceBind, adDNString, userPassword,
ResultCode.SUCCESS));
// Connection should be cached until the policy is finalized.
// Obtain policy and state.
final Entry testUser = TestCaseUtils.makeEntry(
/* @formatter:off */
"dn: " + opendjDNString,
"objectClass: top",
"objectClass: person",
"sn: user",
"cn: test user",
"aduser: " + adDNString,
"uid1: one",
"uid1: two",
"uid1: three"
/* @formatter:on */
);
final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
provider);
assertTrue(factory.isConfigurationAcceptable(cfg, null));
final AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
final AuthenticationPolicyState state = policy
.createAuthenticationPolicyState(testUser);
assertEquals(state.getAuthenticationPolicy(), policy);
// Perform authentication.
assertTrue(state.passwordMatches(ByteString.valueOfUtf8(userPassword)));
// There should be no more pending events.
provider.assertAllExpectedEventsReceived();
state.finalizeStateAfterBind();
// Cached connections should be closed when the policy is finalized.
provider.expectEvent(new CloseEvent(ceSearch));
provider.expectEvent(new CloseEvent(ceBind));
// Tear down and check final state.
policy.finalizeAuthenticationPolicy();
provider.assertAllExpectedEventsReceived();
}
/**
* Tests that mapped PTA performs searches across multiple base DNs if
* configured.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testMultipleSearchBaseDNs() throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
.withPrimaryServer(phost1)
.withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
.withMappedAttribute("uid").withBaseDN("o=first")
.withBaseDN("o=second").withBaseDN("o=third");
// Create the provider and its list of expected events.
final GetLDAPConnectionFactoryEvent fe = new GetLDAPConnectionFactoryEvent(
phost1, cfg);
final MockProvider provider = new MockProvider().expectEvent(fe);
// Add search events.
final GetConnectionEvent ceSearch = new GetConnectionEvent(fe);
provider
.expectEvent(ceSearch)
.expectEvent(
new SimpleBindEvent(ceSearch, searchBindDNString, "searchPassword",
ResultCode.SUCCESS))
.expectEvent(
new SearchEvent(ceSearch, "o=first", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED))
.expectEvent(
new SearchEvent(ceSearch, "o=second", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED))
.expectEvent(
new SearchEvent(ceSearch, "o=third", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", adDNString));
// Connection should be cached until the policy is finalized.
// Add bind events.
final GetConnectionEvent ceBind = new GetConnectionEvent(fe);
provider.expectEvent(ceBind).expectEvent(
new SimpleBindEvent(ceBind, adDNString, userPassword,
ResultCode.SUCCESS));
// Connection should be cached until the policy is finalized.
// Obtain policy and state.
final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
provider);
assertTrue(factory.isConfigurationAcceptable(cfg, null));
final AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
final AuthenticationPolicyState state = policy
.createAuthenticationPolicyState(userEntry);
assertEquals(state.getAuthenticationPolicy(), policy);
// Perform authentication.
assertTrue(state.passwordMatches(ByteString.valueOfUtf8(userPassword)));
// There should be no more pending events.
provider.assertAllExpectedEventsReceived();
state.finalizeStateAfterBind();
// Cached connections should be closed when the policy is finalized.
provider.expectEvent(new CloseEvent(ceSearch));
provider.expectEvent(new CloseEvent(ceBind));
// Tear down and check final state.
policy.finalizeAuthenticationPolicy();
provider.assertAllExpectedEventsReceived();
}
/**
* Test for issue OPENDJ-292
* (https://bugster.forgerock.org/jira/browse/OPENDJ-292). This test checks
* that the last exception is correctly cached in the case where initial
* connection attempts fail.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testIssueOPENDJ292_1() throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
.withPrimaryServer(phost1)
.withSecondaryServer(shost1)
.withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
.withMappedAttribute("uid").withBaseDN("o=ad");
// Create all the events.
final MockProvider provider = new MockProvider();
// First of all the connection factories are created.
final GetLDAPConnectionFactoryEvent fe1 = new GetLDAPConnectionFactoryEvent(
phost1, cfg);
final GetLDAPConnectionFactoryEvent fe2 = new GetLDAPConnectionFactoryEvent(
shost1, cfg);
provider.expectEvent(fe1).expectEvent(fe2);
// Get connection for phost1, then search, then bind.
final GetConnectionEvent ceSearch1 = new GetConnectionEvent(fe1,
ResultCode.CLIENT_SIDE_CONNECT_ERROR);
final GetConnectionEvent ceSearch2 = new GetConnectionEvent(fe2);
provider
.expectEvent(ceSearch1)
.expectEvent(ceSearch2)
.expectEvent(
new SimpleBindEvent(ceSearch2, searchBindDNString,
"searchPassword", ResultCode.INVALID_CREDENTIALS))
.expectEvent(new CloseEvent(ceSearch2));
// Obtain policy and state.
final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
provider);
assertTrue(factory.isConfigurationAcceptable(cfg, null));
final AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
// Authenticate twice, the second time was causing a NPE because none of the
// factories were available and an attempt was made to throw a null
// exception.
for (int i = 0; i < 2; i++)
{
final AuthenticationPolicyState state = policy
.createAuthenticationPolicyState(userEntry);
assertEquals(state.getAuthenticationPolicy(), policy);
try
{
state.passwordMatches(ByteString.valueOfUtf8(userPassword));
fail("password match unexpectedly succeeded");
}
catch (final DirectoryException e)
{
// No mapping attributes so this should always fail with
// INVALID_CREDENTIALS.
assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS,
e.getMessage());
}
state.finalizeStateAfterBind();
}
// Tear down and check final state.
policy.finalizeAuthenticationPolicy();
provider.assertAllExpectedEventsReceived();
}
/**
* Test for issue OPENDJ-292
* (https://bugster.forgerock.org/jira/browse/OPENDJ-292). This test checks
* that the last exception is correctly cached in the case where a usable
* connection fails during a search/bind.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testIssueOPENDJ292_2() throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
.withPrimaryServer(phost1)
.withSecondaryServer(shost1)
.withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
.withMappedAttribute("uid").withBaseDN("o=ad");
// Create all the events.
final MockProvider provider = new MockProvider();
// First of all the connection factories are created.
final GetLDAPConnectionFactoryEvent fe1 = new GetLDAPConnectionFactoryEvent(
phost1, cfg);
final GetLDAPConnectionFactoryEvent fe2 = new GetLDAPConnectionFactoryEvent(
shost1, cfg);
provider.expectEvent(fe1).expectEvent(fe2);
// Get connection for phost1, then search, then bind.
final GetConnectionEvent ceSearch1 = new GetConnectionEvent(fe1);
final GetConnectionEvent ceBind1 = new GetConnectionEvent(fe1);
final GetConnectionEvent ceSearch2 = new GetConnectionEvent(fe2);
provider
.expectEvent(ceSearch1)
.expectEvent(
new SimpleBindEvent(ceSearch1, searchBindDNString, "searchPassword"))
.expectEvent(
new SearchEvent(ceSearch1, "o=ad", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", adDNString))
.expectEvent(ceBind1)
.expectEvent(
new SimpleBindEvent(ceBind1, adDNString, userPassword,
ResultCode.SUCCESS));
// Repeat and fail-over to shost1.
provider
.expectEvent(
new SearchEvent(ceSearch1, "o=ad", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", ResultCode.UNAVAILABLE))
.expectEvent(new CloseEvent(ceSearch1))
.expectEvent(new GetConnectionEvent(fe1, ResultCode.UNAVAILABLE)) // pool retry
.expectEvent(ceSearch2)
.expectEvent(
new SimpleBindEvent(ceSearch2, searchBindDNString, "searchPassword"))
.expectEvent(
new SearchEvent(ceSearch2, "o=ad", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", adDNString))
.expectEvent(
new SimpleBindEvent(ceBind1, adDNString, userPassword,
ResultCode.SUCCESS));
// Repeat, but fail on shost1 as well, leaving no available servers.
provider
.expectEvent(
new SearchEvent(ceSearch2, "o=ad", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", ResultCode.UNAVAILABLE))
.expectEvent(new CloseEvent(ceSearch2))
.expectEvent(new GetConnectionEvent(fe2, ResultCode.UNAVAILABLE)); // pool retry
// Obtain policy and state.
final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
provider);
assertTrue(factory.isConfigurationAcceptable(cfg, null));
final AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
// Authenticate four times, the fourth time was causing a NPE because none
// of the factories were available and an attempt was made to throw a null
// exception.
for (int i = 0; i < 4; i++)
{
final AuthenticationPolicyState state = policy
.createAuthenticationPolicyState(userEntry);
assertEquals(state.getAuthenticationPolicy(), policy);
// Perform authentication.
if (i < 2)
{
assertTrue(state.passwordMatches(ByteString.valueOfUtf8(userPassword)));
}
else
{
try
{
state.passwordMatches(ByteString.valueOfUtf8(userPassword));
fail("password match unexpectedly succeeded");
}
catch (final DirectoryException e)
{
// No mapping attributes so this should always fail with
// INVALID_CREDENTIALS.
assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS,
e.getMessage());
}
}
state.finalizeStateAfterBind();
}
// Cached connections should be closed when the policy is finalized.
provider.expectEvent(new CloseEvent(ceBind1));
// Tear down and check final state.
policy.finalizeAuthenticationPolicy();
provider.assertAllExpectedEventsReceived();
}
/**
* Test for issue OPENDJ-294
* (https://bugster.forgerock.org/jira/browse/OPENDJ-294). Password
* configuration changes do not seem to be taking effect.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@SuppressWarnings("unchecked")
@Test(enabled = true)
public void testIssueOPENDJ294() throws Exception
{
// Mock configurations.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
.withPrimaryServer(phost1)
.withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
.withMappedAttribute("uid").withBaseDN("o=ad");
final LDAPPassThroughAuthenticationPolicyCfg cfg2 = mockCfg()
.withPrimaryServer(phost1)
.withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
.withMappedAttribute("uid").withBaseDN("o=ad")
.withMappedSearchBindPassword("newSearchPassword");
// Create all the events.
final MockProvider provider = new MockProvider();
// First of all the connection factories are created.
final GetLDAPConnectionFactoryEvent fe1 = new GetLDAPConnectionFactoryEvent(
phost1, cfg);
provider.expectEvent(fe1);
// Get connection for phost1, then search, then bind.
final GetConnectionEvent ceSearch1 = new GetConnectionEvent(fe1);
provider
.expectEvent(ceSearch1)
.expectEvent(
new SimpleBindEvent(ceSearch1, searchBindDNString,
"searchPassword", ResultCode.INVALID_CREDENTIALS))
.expectEvent(new CloseEvent(ceSearch1));
// Now simulate monitor thread run which will fail for the same reason.
final GetConnectionEvent ceSearch2 = new GetConnectionEvent(fe1);
provider
.expectEvent(ceSearch2)
.expectEvent(
new SimpleBindEvent(ceSearch1, searchBindDNString,
"searchPassword", ResultCode.INVALID_CREDENTIALS))
.expectEvent(new CloseEvent(ceSearch1));
// Change configuration, re-run monitor (none remaining), and search/bind
// again.
// First of all the connection factories are created.
final GetLDAPConnectionFactoryEvent fe2 = new GetLDAPConnectionFactoryEvent(
phost1, cfg2);
provider.expectEvent(fe2);
final GetConnectionEvent ceSearch3 = new GetConnectionEvent(fe2);
final GetConnectionEvent ceBind1 = new GetConnectionEvent(fe2);
provider
.expectEvent(ceSearch3)
.expectEvent(
new SimpleBindEvent(ceSearch3, searchBindDNString,
"newSearchPassword"))
.expectEvent(
new SearchEvent(ceSearch3, "o=ad", SearchScope.WHOLE_SUBTREE,
"(uid=aduser)", adDNString))
.expectEvent(ceBind1)
.expectEvent(
new SimpleBindEvent(ceBind1, adDNString, userPassword,
ResultCode.SUCCESS));
// Re-run monitor - again nothing to do.
// Connections should be cached until the policy is finalized.
// Obtain policy and state.
final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
provider);
assertTrue(factory.isConfigurationAcceptable(cfg, null));
final AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
// Authenticate twice, correcting the configuration in between.
final AuthenticationPolicyState state = policy
.createAuthenticationPolicyState(userEntry);
assertEquals(state.getAuthenticationPolicy(), policy);
// Perform authentication.
try
{
state.passwordMatches(ByteString.valueOfUtf8(userPassword));
fail("password match unexpectedly succeeded");
}
catch (final DirectoryException e)
{
// No mapping attributes so this should always fail with
// INVALID_CREDENTIALS.
assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS,
e.getMessage());
}
state.finalizeStateAfterBind();
// Run monitor which should try to connect to phost1 and fail again.
provider.runMonitorTasks();
// Change the configuration, fixing the password.
((ConfigurationChangeListener<LDAPPassThroughAuthenticationPolicyCfg>) policy)
.applyConfigurationChange(cfg2);
// Run monitor which should now have nothing to do.
provider.runMonitorTasks();
// Perform authentication, which should now succeed.
final AuthenticationPolicyState state2 = policy
.createAuthenticationPolicyState(userEntry);
assertEquals(state2.getAuthenticationPolicy(), policy);
assertTrue(state2.passwordMatches(ByteString.valueOfUtf8(userPassword)));
state2.finalizeStateAfterBind();
// Run monitor which should still have nothing to do.
provider.runMonitorTasks();
// Cached connections should be closed when the policy is finalized.
provider.expectEvent(new CloseEvent(ceSearch3));
provider.expectEvent(new CloseEvent(ceBind1));
// Tear down and check final state.
policy.finalizeAuthenticationPolicy();
provider.assertAllExpectedEventsReceived();
}
/**
* Test for issue OPENDJ-290
* (https://bugster.forgerock.org/jira/browse/OPENDJ-290).
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true)
public void testIssueOPENDJ290() throws Exception
{
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
.withPrimaryServer(phost1)
.withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
.withMappedAttribute("uid").withBaseDN("o=ad");
// Create all the events.
final MockProvider provider = new MockProvider();
// First of all the connection factories are created.
final GetLDAPConnectionFactoryEvent fe1 = new GetLDAPConnectionFactoryEvent(
phost1, cfg);
provider.expectEvent(fe1);
// Get connection then bind twice creating two pooled connections.
final GetConnectionEvent ce1 = new GetConnectionEvent(fe1);
final GetConnectionEvent ce2 = new GetConnectionEvent(fe1);
provider
.expectEvent(ce1)
.expectEvent(
new SimpleBindEvent(ce1, searchBindDNString, "searchPassword"))
.expectEvent(ce2)
.expectEvent(
new SimpleBindEvent(ce2, searchBindDNString, "searchPassword"));
// Once two pooled connections are created, retry but this time simulate the
// first connection failing. The attempt should automatically retry on a new
// connection.
final GetConnectionEvent ce3 = new GetConnectionEvent(fe1);
provider
.expectEvent(
new SimpleBindEvent(ce1, searchBindDNString, "searchPassword",
ResultCode.UNAVAILABLE))
.expectEvent(new CloseEvent(ce1))
.expectEvent(ce3)
.expectEvent(
new SimpleBindEvent(ce3, searchBindDNString, "searchPassword"));
// Use a connection pool directly for this test.
ConnectionPool pool = new ConnectionPool(provider.getLDAPConnectionFactory(
"phost1", 11, cfg));
// Authenticate three times, the third time was failing because the pool
// would not retry the operation on a new connection.
ByteString username = ByteString.valueOfUtf8(searchBindDNString);
ByteString password = ByteString.valueOfUtf8("searchPassword");
Connection c1 = pool.getConnection();
c1.simpleBind(username, password);
// Don't release c1 because we want to force creation of a second connection.
Connection c2 = pool.getConnection();
c2.simpleBind(username, password);
// Release both the connections.
c1.close();
c2.close();
// This was failing because the pool would not retry with a new connection.
Connection c3 = pool.getConnection();
c3.simpleBind(username, password);
c3.close();
// There should be no more pending events.
provider.assertAllExpectedEventsReceived();
// Cached connections should be closed when the pool is closed.
provider.expectEvent(new CloseEvent(ce2));
provider.expectEvent(new CloseEvent(ce3));
// Tear down and check final state.
pool.close();
provider.assertAllExpectedEventsReceived();
}
/**
* Returns test data for {@link #testPasswordCaching}.
*
* @return Test data for {@link #testPasswordCaching}.
*/
@DataProvider
public Object[][] testPasswordCachingData()
{
// @formatter:off
return new Object[][] {
/* cacheState, matchesCache, matchesReal */
{ "notPresent", false, false },
{ "notPresent", false, true },
{ "notPresent", true, false },
{ "notPresent", true, true },
{ "presentValid", false, false },
{ "presentValid", false, true },
{ "presentValid", true, false },
{ "presentValid", true, true },
{ "presentExpired", false, false },
{ "presentExpired", false, true },
{ "presentExpired", true, false },
{ "presentExpired", true, true },
};
// @formatter:on
}
/**
* Tests password caching functionality.
*
* @param cacheState
* A string indicating the state of the cached password in the user's
* entry.
* @param matchesCache
* {@code true} if the presented password should match the cached
* password.
* @param matchesReal
* {@code true} if the presented password should match the real
* password.
* @throws Exception
* If an unexpected exception occurred.
*/
@Test(enabled = true, dataProvider = "testPasswordCachingData")
public void testPasswordCaching(String cacheState, boolean matchesCache,
boolean matchesReal) throws Exception
{
// Create an empty test backend 'o=test'
TestCaseUtils.initializeTestBackend(true);
// Choose arbitrary date.
final String testCurrentTimeGMT = "20100621120000Z";
final boolean expectPTA;
final boolean expectCacheInfo;
final boolean expectCacheUpdate;
final boolean expectedBindResultIsSuccess;
final Entry testUser;
final String testCachedPassword = "{SSHA}N8QSu9kXHkODFxFtwtwVEqM5XMCfnSaq/5gWew==";
final String testCachedPasswordTime;
if (cacheState.equals("notPresent"))
{
expectPTA = true;
expectCacheInfo = matchesReal;
expectCacheUpdate = matchesReal;
expectedBindResultIsSuccess = matchesReal;
testCachedPasswordTime = null;
testUser = TestCaseUtils.makeEntry(
/* @formatter:off */
"dn: cn=test user,o=test",
"objectClass: top",
"objectClass: person",
"sn: user",
"cn: test user"
/* @formatter:on */
);
}
else if (cacheState.equals("presentValid"))
{
expectPTA = !matchesCache;
expectCacheInfo = true;
expectCacheUpdate = !matchesCache && matchesReal;
expectedBindResultIsSuccess = matchesCache | matchesReal;
testCachedPasswordTime = "20100621110000Z"; // 1 hour old
// Create an entry whose cached password is 10s old.
testUser = TestCaseUtils.makeEntry(
/* @formatter:off */
"dn: cn=test user,o=test",
"objectClass: top",
"objectClass: person",
"sn: user",
"cn: test user",
"ds-pta-cached-password: " + testCachedPassword,
"ds-pta-cached-password-time: " + testCachedPasswordTime
/* @formatter:on */
);
}
else
{
// presentExpired
expectPTA = true;
expectCacheInfo = true;
expectCacheUpdate = matchesReal;
expectedBindResultIsSuccess = matchesReal;
testCachedPasswordTime = "20100620110000Z"; // 1 day + 1 hour old
// Create an entry whose cached password is more than 1 day old.
testUser = TestCaseUtils.makeEntry(
/* @formatter:off */
"dn: cn=test user,o=test",
"objectClass: top",
"objectClass: person",
"sn: user",
"cn: test user",
"ds-pta-cached-password: " + testCachedPassword,
"ds-pta-cached-password-time: " + testCachedPasswordTime
/* @formatter:on */
);
}
final String presentedPassword;
if (matchesCache)
{
presentedPassword = "password";
}
else
{
presentedPassword = "doesNotMatchCache";
}
// Add the test entry.
TestCaseUtils.addEntry(testUser);
// Mock configuration.
final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
.withPrimaryServer(phost1).withUsePasswordCaching(true);
// Create all the events.
final GetLDAPConnectionFactoryEvent fe = new GetLDAPConnectionFactoryEvent(
phost1, cfg);
final MockProvider provider = new MockProvider().withCurrentTime(
testCurrentTimeGMT).expectEvent(fe);
if (expectPTA)
{
// Get connection then bind twice creating two pooled connections.
final GetConnectionEvent ce = new GetConnectionEvent(fe);
provider
.expectEvent(ce)
.expectEvent(
new SimpleBindEvent(ce, "cn=test user,o=test", presentedPassword,
expectedBindResultIsSuccess ? ResultCode.SUCCESS
: ResultCode.INVALID_CREDENTIALS))
.expectEvent(new CloseEvent(ce));
}
// Obtain policy and state.
final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
provider);
assertTrue(factory.isConfigurationAcceptable(cfg, null));
final AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
// Perform the authentication.
final AuthenticationPolicyState state = policy
.createAuthenticationPolicyState(testUser);
assertEquals(state.getAuthenticationPolicy(), policy);
assertEquals(state.passwordMatches(ByteString.valueOfUtf8(presentedPassword)),
expectedBindResultIsSuccess);
state.finalizeStateAfterBind();
// Check that the password has been cached if needed.
Entry updatedTestUser = DirectoryServer.getEntry(DN
.valueOf("cn=test user,o=test"));
String newCachedPassword =
updatedTestUser.parseAttribute("ds-pta-cached-password").asString();
String newCachedPasswordTime =
updatedTestUser.parseAttribute("ds-pta-cached-password-time").asString();
if (expectCacheInfo)
{
assertNotNull(newCachedPassword);
assertNotNull(newCachedPasswordTime);
if (expectCacheUpdate)
{
assertFalse(newCachedPassword.equals(testCachedPassword));
assertTrue(UserPasswordSyntax.isEncoded(ByteString
.valueOfUtf8(newCachedPassword)));
assertEquals(newCachedPasswordTime, testCurrentTimeGMT);
}
else
{
assertEquals(newCachedPassword, testCachedPassword);
assertEquals(newCachedPasswordTime, testCachedPasswordTime);
}
}
else
{
assertNull(newCachedPassword);
assertNull(newCachedPasswordTime);
}
// Tear down and check final state.
policy.finalizeAuthenticationPolicy();
provider.assertAllExpectedEventsReceived();
}
MockPolicyCfg mockCfg()
{
return new MockPolicyCfg();
}
MockServer mockServer() throws IOException
{
final ServerSocket serverSocket = TestCaseUtils.bindFreePort();
return new MockServer(serverSocket);
}
BindRequestProtocolOp newBindRequest(final String dn, final String password)
throws LDAPException
{
return new BindRequestProtocolOp(ByteString.valueOfUtf8(dn), 3,
ByteString.valueOfUtf8(password));
}
BindResponseProtocolOp newBindResult(final ResultCode resultCode)
{
return new BindResponseProtocolOp(resultCode.intValue());
}
ExtendedResponseProtocolOp newDisconnectNotification(
final ResultCode resultCode)
{
return new ExtendedResponseProtocolOp(resultCode.intValue(), null, null,
null, OID_NOTICE_OF_DISCONNECTION, null);
}
SearchResultEntryProtocolOp newSearchEntry(final String dn)
throws DirectoryException
{
return new SearchResultEntryProtocolOp(DN.valueOf(dn));
}
SearchRequestProtocolOp newSearchRequest(final String dn,
final String filter, final LDAPPassThroughAuthenticationPolicyCfg cfg)
throws LDAPException
{
final int timeout = (int) (cfg.getConnectionTimeout() / 1000);
return new SearchRequestProtocolOp(ByteString.valueOfUtf8(dn),
SearchScope.WHOLE_SUBTREE, DereferenceAliasesPolicy.ALWAYS, 1, timeout,
true, RawFilter.create(filter),
LDAPPassThroughAuthenticationPolicyFactory.NO_ATTRIBUTES);
}
SearchResultDoneProtocolOp newSearchResult(final ResultCode resultCode)
{
return new SearchResultDoneProtocolOp(resultCode.intValue());
}
}