From eca35e3446c4f14521f56f56ce7ea64fc22a5652 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Fri, 02 Sep 2011 14:07:25 +0000
Subject: [PATCH] OPENDJ-262: Implement pass through authentication (PTA)

---
 opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java |  431 ++++++++++++++++++++++++++++++++++++++++++-----------
 1 files changed, 341 insertions(+), 90 deletions(-)

diff --git a/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java b/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java
index 36e9cc3..afddeb6 100644
--- a/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java
+++ b/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java
@@ -51,6 +51,7 @@
 import org.opends.server.types.*;
 import org.opends.server.util.StaticUtils;
 import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
 
@@ -62,6 +63,12 @@
     ExtensionsTestCase
 {
 
+  // TODO: multiple search base DNs
+  // TODO: multiple match attributes/values
+  // TODO: connection pooling
+  // TODO: load balancing
+  // TODO: fail-over
+
   static class CloseEvent extends Event<Void>
   {
     private final GetConnectionEvent getConnectionEvent;
@@ -311,8 +318,8 @@
     public ByteString search(DN baseDN, SearchScope scope, SearchFilter filter)
         throws DirectoryException
     {
-      SearchEvent event = new SearchEvent(getConnectionEvent, baseDN, scope,
-          filter);
+      SearchEvent event = new SearchEvent(getConnectionEvent,
+          baseDN.toString(), scope, filter.toString());
       Object result = mockProvider.assertNextEventExpected(event);
       if (result instanceof ByteString)
       {
@@ -656,42 +663,41 @@
 
   static class SearchEvent extends Event<Object>
   {
-    private final DN baseDN;
+    private final String baseDN;
     private final GetConnectionEvent cevent;
-    private final SearchFilter filter;
+    private final String filter;
     private final ResultCode resultCode;
     private final SearchScope scope;
-    private final ByteString username;
+    private final String username;
 
 
 
-    SearchEvent(GetConnectionEvent cevent, DN baseDN, SearchScope scope,
-        SearchFilter filter)
+    SearchEvent(GetConnectionEvent cevent, String baseDN, SearchScope scope,
+        String filter)
     {
       this(cevent, baseDN, scope, filter, null, ResultCode.SUCCESS);
     }
 
 
 
-    SearchEvent(GetConnectionEvent cevent, DN baseDN, SearchScope scope,
-        SearchFilter filter, ByteString username)
+    SearchEvent(GetConnectionEvent cevent, String baseDN, SearchScope scope,
+        String filter, String username)
     {
       this(cevent, baseDN, scope, filter, username, ResultCode.SUCCESS);
     }
 
 
 
-    SearchEvent(GetConnectionEvent cevent, DN baseDN, SearchScope scope,
-        SearchFilter filter, ResultCode resultCode)
+    SearchEvent(GetConnectionEvent cevent, String baseDN, SearchScope scope,
+        String filter, ResultCode resultCode)
     {
       this(cevent, baseDN, scope, filter, null, resultCode);
     }
 
 
 
-    private SearchEvent(GetConnectionEvent cevent, DN baseDN,
-        SearchScope scope, SearchFilter filter, ByteString username,
-        ResultCode resultCode)
+    private SearchEvent(GetConnectionEvent cevent, String baseDN,
+        SearchScope scope, String filter, String username, ResultCode resultCode)
     {
       this.cevent = cevent;
       this.baseDN = baseDN;
@@ -848,7 +854,7 @@
   private final String shost3 = "shost3:33";
 
   private DN policyDN;
-  private final String policyDNString = "cn=test mappingPolicy,o=test";
+  private final String policyDNString = "cn=test policy,o=test";
 
   private DN searchBindDN;
   private final String searchBindDNString = "cn=search bind dn";
@@ -856,7 +862,7 @@
   private DN trustManagerDN;
   private final String trustManagerDNString = "cn=ignored";
 
-  private final String adDNString = "cn=ad user,o=ad";
+  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";
@@ -894,34 +900,60 @@
 
 
   /**
-   * Tests that initial connection errors are handled properly.
+   * 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 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 = false)
-  public void testInitialConnectionFailure() throws Exception
+  @Test(enabled = false, dataProvider = "testConnectionFailureDuringSearchGetConnectionData")
+  public void testConnectionFailureDuringSearchGetConnection(
+      ResultCode connectResultCode) throws Exception
   {
-    LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg().withPrimaryServer(
-        phost1);
+    // Mock configuration.
+    LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
+        .withPrimaryServer(phost1)
+        .withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
+        .withMappedAttribute("uid").withBaseDN("o=ad");
 
+    // Create the provider and its list of expected events.
     GetLDAPConnectionFactoryEvent fe = new GetLDAPConnectionFactoryEvent(
         phost1, cfg);
-    GetConnectionEvent ce = new GetConnectionEvent(fe, ResultCode.UNAVAILABLE);
+    GetConnectionEvent ceSearch = new GetConnectionEvent(fe, connectResultCode);
 
     MockProvider provider = new MockProvider().withExpectedEvent(fe)
-        .withExpectedEvent(ce);
+        .withExpectedEvent(ceSearch);
 
+    // Obtain policy and state.
     LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
         provider);
-
     assertTrue(factory.isConfigurationAcceptable(cfg, null));
-
     AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
     AuthenticationPolicyState state = policy
         .createAuthenticationPolicyState(userEntry);
-
     assertEquals(state.getAuthenticationPolicy(), policy);
+
+    // Perform authentication.
     try
     {
       state.passwordMatches(ByteString.valueOf(userPassword));
@@ -929,49 +961,83 @@
     }
     catch (DirectoryException e)
     {
+      // No valid connections available so this should always fail with
+      // UNAVAILABLE.
       assertEquals(e.getResultCode(), ResultCode.UNAVAILABLE);
     }
 
+    // Tear down and check final state.
+    provider.assertNoMoreEvents();
     state.finalizeStateAfterBind();
     policy.finalizeAuthenticationPolicy();
-    provider.assertNoMoreEvents();
   }
 
 
 
   /**
-   * Tests the unmapped policy where the connection fails during bind.
+   * 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
+  }
+
+
+
+  /**
+   * 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 = false)
-  public void testUnmappedPolicyConnectionFailure() throws Exception
+  @Test(enabled = false, dataProvider = "testConnectionFailureDuringSearchBindData")
+  public void testConnectionFailureDuringSearchBind(ResultCode bindResultCode)
+      throws Exception
   {
-    LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg().withPrimaryServer(
-        phost1);
+    // Mock configuration.
+    LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
+        .withPrimaryServer(phost1)
+        .withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
+        .withMappedAttribute("uid").withBaseDN("o=ad");
 
+    // Create the provider and its list of expected events.
     GetLDAPConnectionFactoryEvent fe = new GetLDAPConnectionFactoryEvent(
         phost1, cfg);
-    GetConnectionEvent ce = new GetConnectionEvent(fe);
+    GetConnectionEvent ceSearch = new GetConnectionEvent(fe);
 
     MockProvider provider = new MockProvider()
         .withExpectedEvent(fe)
-        .withExpectedEvent(ce)
+        .withExpectedEvent(ceSearch)
         .withExpectedEvent(
-            new SimpleBindEvent(ce, opendjDNString, userPassword,
-                ResultCode.UNAVAILABLE)).withExpectedEvent(new CloseEvent(ce));
+            new SimpleBindEvent(ceSearch, searchBindDNString, "searchPassword",
+                bindResultCode)).withExpectedEvent(new CloseEvent(ceSearch));
 
+    // Obtain policy and state.
     LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
         provider);
-
     assertTrue(factory.isConfigurationAcceptable(cfg, null));
-
     AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
     AuthenticationPolicyState state = policy
         .createAuthenticationPolicyState(userEntry);
-
     assertEquals(state.getAuthenticationPolicy(), policy);
+
+    // Perform authentication.
     try
     {
       state.passwordMatches(ByteString.valueOf(userPassword));
@@ -979,53 +1045,253 @@
     }
     catch (DirectoryException e)
     {
+      // No valid connections available so this should always fail with
+      // UNAVAILABLE.
       assertEquals(e.getResultCode(), ResultCode.UNAVAILABLE);
     }
 
+    // Tear down and check final state.
+    provider.assertNoMoreEvents();
     state.finalizeStateAfterBind();
     policy.finalizeAuthenticationPolicy();
+  }
+
+
+
+  /**
+   * 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.UNAVAILABLE }
+    };
+    // @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 = false, dataProvider = "testConnectionFailureDuringSearchData")
+  public void testConnectionFailureDuringSearch(ResultCode searchResultCode)
+      throws Exception
+  {
+    // Mock configuration.
+    LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
+        .withPrimaryServer(phost1)
+        .withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
+        .withMappedAttribute("uid").withBaseDN("o=ad");
+
+    // Create the provider and its list of expected events.
+    GetLDAPConnectionFactoryEvent fe = new GetLDAPConnectionFactoryEvent(
+        phost1, cfg);
+    GetConnectionEvent ceSearch = new GetConnectionEvent(fe);
+
+    MockProvider provider = new MockProvider()
+        .withExpectedEvent(fe)
+        .withExpectedEvent(ceSearch)
+        .withExpectedEvent(
+            new SimpleBindEvent(ceSearch, searchBindDNString, "searchPassword",
+                ResultCode.SUCCESS))
+        .withExpectedEvent(
+            new SearchEvent(ceSearch, "o=ad", SearchScope.WHOLE_SUBTREE,
+                "(uid=aduser)", searchResultCode));
+    if (isFatalResultCode(searchResultCode))
+    {
+      // The connection will fail and be closed immediately.
+      provider.withExpectedEvent(new CloseEvent(ceSearch));
+    }
+
+    // Obtain policy and state.
+    LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
+        provider);
+    assertTrue(factory.isConfigurationAcceptable(cfg, null));
+    AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
+    AuthenticationPolicyState state = policy
+        .createAuthenticationPolicyState(userEntry);
+    assertEquals(state.getAuthenticationPolicy(), policy);
+
+    // Perform authentication.
+    try
+    {
+      state.passwordMatches(ByteString.valueOf(userPassword));
+      fail("password match unexpectedly succeeded");
+    }
+    catch (DirectoryException e)
+    {
+      // No valid connections available so this should always fail with
+      // UNAVAILABLE.
+      assertEquals(e.getResultCode(), ResultCode.UNAVAILABLE);
+    }
+
+    // There should be no more pending events.
+    provider.assertNoMoreEvents();
+    state.finalizeStateAfterBind();
+
+    // Cached connections should be closed when the policy is finalized.
+    if (!isFatalResultCode(searchResultCode))
+    {
+      provider.withExpectedEvent(new CloseEvent(ceSearch));
+    }
+
+    // Tear down and check final state.
+    policy.finalizeAuthenticationPolicy();
     provider.assertNoMoreEvents();
   }
 
 
 
   /**
-   * Tests the unmapped policy with invalid credentials.
+   * Returns test data for {@link #testMappingPolicyAuthentication}.
    *
+   * @return Test data for {@link #testMappingPolicyAuthentication}.
+   */
+  @DataProvider
+  public Object[][] testMappingPolicyAuthenticationData()
+  {
+    // @formatter:off
+    return new Object[][] {
+        /* policy, connection failure, invalid credentials */
+        { 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 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 = false)
-  public void testUnmappedPolicyInvalidCredentials() throws Exception
+  @Test(enabled = false, dataProvider = "testMappingPolicyAuthenticationData")
+  public void testMappingPolicyAuthentication(MappingPolicy mappingPolicy,
+      ResultCode bindResultCode) throws Exception
   {
-    LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg().withPrimaryServer(
-        phost1);
+    // Mock configuration.
+    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.
     GetLDAPConnectionFactoryEvent fe = new GetLDAPConnectionFactoryEvent(
         phost1, cfg);
-    GetConnectionEvent ce = new GetConnectionEvent(fe);
+    MockProvider provider = new MockProvider().withExpectedEvent(fe);
 
-    MockProvider provider = new MockProvider()
-        .withExpectedEvent(fe)
-        .withExpectedEvent(ce)
-        .withExpectedEvent(
-            new SimpleBindEvent(ce, opendjDNString, userPassword,
-                ResultCode.INVALID_CREDENTIALS))
-        .withExpectedEvent(new CloseEvent(ce));
+    // Add search events if doing a mapped search.
+    GetConnectionEvent ceSearch = null;
+    if (mappingPolicy == MappingPolicy.MAPPED_SEARCH)
+    {
+      ceSearch = new GetConnectionEvent(fe);
+      provider.withExpectedEvent(
+          new SimpleBindEvent(ceSearch, searchBindDNString, "searchPassword",
+              ResultCode.SUCCESS)).withExpectedEvent(
+          new SearchEvent(ceSearch, "o=ad", SearchScope.WHOLE_SUBTREE,
+              "(uid=aduser)", adDNString));
+      // Connection should be cached until the policy is finalized.
+    }
 
+    // Add bind events.
+    GetConnectionEvent ceBind = new GetConnectionEvent(fe);
+    provider.withExpectedEvent(ceBind).withExpectedEvent(
+        new SimpleBindEvent(ceBind,
+            mappingPolicy == MappingPolicy.UNMAPPED ? opendjDNString
+                : adDNString, userPassword, bindResultCode));
+    if (isFatalResultCode(bindResultCode))
+    {
+      // The connection will fail and be closed immediately.
+      provider.withExpectedEvent(new CloseEvent(ceBind));
+    }
+
+    // Connection should be cached until the policy is finalized or until the
+    // connection fails.
+
+    // Obtain policy and state.
     LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
         provider);
-
     assertTrue(factory.isConfigurationAcceptable(cfg, null));
-
     AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
     AuthenticationPolicyState state = policy
         .createAuthenticationPolicyState(userEntry);
-
     assertEquals(state.getAuthenticationPolicy(), policy);
-    assertFalse(state.passwordMatches(ByteString.valueOf(userPassword)));
 
+    // Perform authentication.
+    switch (bindResultCode)
+    {
+    case SUCCESS:
+      assertTrue(state.passwordMatches(ByteString.valueOf(userPassword)));
+      break;
+    case INVALID_CREDENTIALS:
+      assertFalse(state.passwordMatches(ByteString.valueOf(userPassword)));
+      break;
+    default:
+      try
+      {
+        state.passwordMatches(ByteString.valueOf(userPassword));
+        fail("password match did not fail");
+      }
+      catch (DirectoryException e)
+      {
+        // No authentication related error codes should be mapped to
+        // UNAVAILABLE.
+        assertEquals(e.getResultCode(), ResultCode.UNAVAILABLE);
+      }
+      break;
+    }
+
+    // There should be no more pending events.
+    provider.assertNoMoreEvents();
     state.finalizeStateAfterBind();
+
+    // Cached connections should be closed when the policy is finalized.
+    if (ceSearch != null)
+    {
+      provider.withExpectedEvent(new CloseEvent(ceSearch));
+    }
+    if (!isFatalResultCode(bindResultCode))
+    {
+      provider.withExpectedEvent(new CloseEvent(ceBind));
+    }
+
+    // Tear down and check final state.
     policy.finalizeAuthenticationPolicy();
     provider.assertNoMoreEvents();
   }
@@ -1033,43 +1299,28 @@
 
 
   /**
-   * Tests the unmapped policy with valid credentials.
+   * Determines whether or no a result code is expected to trigger the
+   * associated connection to be closed immediately.
    *
-   * @throws Exception
-   *           If an unexpected exception occurred.
+   * @param resultCode
+   *          The result code.
+   * @return {@code true} if the result code is expected to trigger the
+   *         associated connection to be closed immediately.
    */
-  @Test(enabled = false)
-  public void testUnmappedPolicyValidCredentials() throws Exception
+  private boolean isFatalResultCode(ResultCode resultCode)
   {
-    LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg().withPrimaryServer(
-        phost1);
-
-    GetLDAPConnectionFactoryEvent fe = new GetLDAPConnectionFactoryEvent(
-        phost1, cfg);
-    GetConnectionEvent ce = new GetConnectionEvent(fe);
-
-    MockProvider provider = new MockProvider()
-        .withExpectedEvent(fe)
-        .withExpectedEvent(ce)
-        .withExpectedEvent(
-            new SimpleBindEvent(ce, opendjDNString, userPassword))
-        .withExpectedEvent(new CloseEvent(ce));
-
-    LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
-        provider);
-
-    assertTrue(factory.isConfigurationAcceptable(cfg, null));
-
-    AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
-    AuthenticationPolicyState state = policy
-        .createAuthenticationPolicyState(userEntry);
-
-    assertEquals(state.getAuthenticationPolicy(), policy);
-    assertTrue(state.passwordMatches(ByteString.valueOf(userPassword)));
-
-    state.finalizeStateAfterBind();
-    policy.finalizeAuthenticationPolicy();
-    provider.assertNoMoreEvents();
+    switch (resultCode)
+    {
+    case BUSY:
+    case UNAVAILABLE:
+    case PROTOCOL_ERROR:
+    case OTHER:
+    case UNWILLING_TO_PERFORM:
+    case OPERATIONS_ERROR:
+      return true;
+    default:
+      return false;
+    }
   }
 
 

--
Gitblit v1.10.0