From d4262475f3e099e1023ba0e8c9ba53782268f52a Mon Sep 17 00:00:00 2001
From: ian.packer <ian.packer@forgerock.com>
Date: Fri, 24 Jun 2016 12:51:57 +0000
Subject: [PATCH] OPENDJ-1103 OPENDJ-1626 Allow configuration of a filter template for passthrough authentication mapped searches

---
 opendj-server-legacy/src/main/java/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java                           |   41 ++++++++++
 opendj-server-legacy/resource/schema/02-config.ldif                                                                                       |    7 +
 opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/LDAPPassThroughAuthenticationPolicyConfiguration.xml |   33 ++++++++
 opendj-server-legacy/src/messages/org/opends/messages/extension.properties                                                                |    2 
 opendj-server-legacy/src/test/java/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java                          |  149 +++++++++++++++++++++++++++++++++++++
 5 files changed, 230 insertions(+), 2 deletions(-)

diff --git a/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/LDAPPassThroughAuthenticationPolicyConfiguration.xml b/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/LDAPPassThroughAuthenticationPolicyConfiguration.xml
index 9935cb6..3e46b2c 100644
--- a/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/LDAPPassThroughAuthenticationPolicyConfiguration.xml
+++ b/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/LDAPPassThroughAuthenticationPolicyConfiguration.xml
@@ -12,7 +12,7 @@
   Header, with the fields enclosed by brackets [] replaced by your own identifying
   information: "Portions Copyright [year] [name of copyright owner]".
 
-  Copyright 2011-2015 ForgeRock AS.
+  Copyright 2011-2016 ForgeRock AS.
   ! -->
 <adm:managed-object name="ldap-pass-through-authentication-policy"
   plural-name="ldap-pass-through-authentication-policies" extends="authentication-policy"
@@ -370,6 +370,37 @@
     </adm:profile>
   </adm:property>
 
+  <adm:property name="mapped-search-filter-template">
+    <adm:synopsis>
+      If defined, overrides the filter used when searching for the user, substituting
+      %s with the value of the local entry's "mapped-attribute".
+    </adm:synopsis>
+    <adm:description>
+      The filter-template may include ZERO or ONE %s substitutions.
+
+      If multiple mapped-attributes are configured, multiple renditions of this template
+      will be aggregated into one larger filter using an OR (|) operator.
+
+      An example use-case for this property would be to use a different attribute type
+      on the mapped search. For example, mapped-attribute could be set to "uid" and
+      filter-template to "(samAccountName=%s)".
+
+      You can also use the filter to restrict search results.
+      For example: "(&amp;(uid=%s)(objectclass=student))"
+    </adm:description>
+    <adm:default-behavior>
+      <adm:undefined/>
+    </adm:default-behavior>
+    <adm:syntax>
+      <adm:string />
+    </adm:syntax>
+    <adm:profile name="ldap">
+      <ldap:attribute>
+        <ldap:name>ds-cfg-mapped-search-filter-template</ldap:name>
+      </ldap:attribute>
+    </adm:profile>
+  </adm:property>
+
   <adm:property name="mapped-search-bind-password">
     <adm:synopsis>
       Specifies the bind password which should be used to perform
diff --git a/opendj-server-legacy/resource/schema/02-config.ldif b/opendj-server-legacy/resource/schema/02-config.ldif
index 571d451..6c8531a 100644
--- a/opendj-server-legacy/resource/schema/02-config.ldif
+++ b/opendj-server-legacy/resource/schema/02-config.ldif
@@ -3937,6 +3937,12 @@
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
   SINGLE-VALUE
   X-ORIGIN 'OpenDJ Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.184
+  NAME 'ds-cfg-mapped-search-filter-template'
+  EQUALITY caseIgnoreMatch
+  SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+  SINGLE-VALUE
+  X-ORIGIN 'OpenDJ Directory Server' )
 objectClasses: ( 1.3.6.1.4.1.26027.1.2.1
   NAME 'ds-cfg-access-control-handler'
   SUP top
@@ -5683,6 +5689,7 @@
         ds-cfg-mapped-search-bind-password-environment-variable $
         ds-cfg-mapped-search-bind-password-file $
         ds-cfg-mapped-search-base-dn $
+        ds-cfg-mapped-search-filter-template $
         ds-cfg-connection-timeout $
         ds-cfg-trust-manager-provider $
         ds-cfg-use-ssl $
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java b/opendj-server-legacy/src/main/java/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
index 011ced1..17d3df2 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
@@ -34,6 +34,8 @@
 import java.util.List;
 import java.util.Queue;
 import java.util.Set;
+import java.util.IllegalFormatConversionException;
+import java.util.MissingFormatArgumentException;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
@@ -66,6 +68,7 @@
 import org.forgerock.opendj.ldap.ModificationType;
 import org.forgerock.opendj.ldap.ResultCode;
 import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.Filter;
 import org.forgerock.opendj.ldap.schema.AttributeType;
 import org.forgerock.opendj.server.config.meta.LDAPPassThroughAuthenticationPolicyCfgDefn.MappingPolicy;
 import org.forgerock.opendj.server.config.server.LDAPPassThroughAuthenticationPolicyCfg;
@@ -1477,6 +1480,8 @@
             // A search against the remote directory is required in order to
             // determine the bind DN.
 
+            final String filterTemplate =  cfg.getMappedSearchFilterTemplate();
+
             // Construct the search filter.
             final LinkedList<SearchFilter> filterComponents = new LinkedList<>();
             for (final AttributeType at : cfg.getMappedAttribute())
@@ -1485,7 +1490,15 @@
               {
                 for (final ByteString value : attribute)
                 {
-                  filterComponents.add(SearchFilter.createEqualityFilter(at, value));
+                  if (filterTemplate != null)
+                  {
+                    filterComponents.add(SearchFilter.createFilterFromString(
+                        Filter.format(filterTemplate, value).toString()));
+                  }
+                  else
+                  {
+                    filterComponents.add(SearchFilter.createEqualityFilter(at, value));
+                  }
                 }
               }
             }
@@ -2054,6 +2067,26 @@
     return password;
   }
 
+  private static boolean isMappedFilterTemplateValid(
+      final String filterTemplate,
+      final List<LocalizableMessage> unacceptableReasons)
+  {
+    if (filterTemplate != null)
+    {
+      try
+      {
+        Filter.format(filterTemplate, "testValue");
+      }
+      catch(IllegalFormatConversionException | MissingFormatArgumentException | LocalizedIllegalArgumentException e)
+      {
+        unacceptableReasons.add(ERR_LDAP_PTA_INVALID_FILTER_TEMPLATE.get(filterTemplate));
+        return false;
+      }
+    }
+
+    return true;
+  }
+
   private static boolean isServerAddressValid(
       final LDAPPassThroughAuthenticationPolicyCfg configuration,
       final List<LocalizableMessage> unacceptableReasons, final String hostPort)
@@ -2170,6 +2203,12 @@
       configurationIsAcceptable = false;
     }
 
+    if (!isMappedFilterTemplateValid(cfg.getMappedSearchFilterTemplate(),
+        unacceptableReasons))
+    {
+      configurationIsAcceptable = false;
+    }
+
     return configurationIsAcceptable;
   }
 }
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/extension.properties b/opendj-server-legacy/src/messages/org/opends/messages/extension.properties
index 748d73b..f09e4a6 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/extension.properties
+++ b/opendj-server-legacy/src/messages/org/opends/messages/extension.properties
@@ -956,3 +956,5 @@
  the aliase(s) '%s' \ to contain key(s) of type(s) '%s'.
 ERR_PWSCHEME_INVALID_STORED_PASSWORD_638=An error occurred while attempting \
  to match a bcrypt hashed password value:  %s
+ERR_LDAP_PTA_INVALID_FILTER_TEMPLATE_639=The mapped search filter template "%s" \
+ could not be parsed as a valid LDAP filter
\ No newline at end of file
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java
index c081275..913743c 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java
@@ -43,6 +43,7 @@
 import org.forgerock.opendj.ldap.ByteStringBuilder;
 import org.forgerock.opendj.ldap.DN;
 import org.forgerock.opendj.ldap.DereferenceAliasesPolicy;
+import org.forgerock.opendj.ldap.Filter;
 import org.forgerock.opendj.ldap.ResultCode;
 import org.forgerock.opendj.ldap.SearchScope;
 import org.forgerock.opendj.ldap.schema.AttributeType;
@@ -331,6 +332,7 @@
     private int timeoutMS;
     private DN mappedSearchBindDN = searchBindDN;
     private String mappedSearchBindPassword = "searchPassword";
+    private String mappedSearchFilterTemplate;
     private String mappedSearchBindPasswordEnvVar;
     private String mappedSearchBindPasswordFile;
     private String mappedSearchBindPasswordProperty;
@@ -535,6 +537,12 @@
       return this;
     }
 
+    MockPolicyCfg withMappedSearchFilterTemplate(final String value)
+    {
+      this.mappedSearchFilterTemplate = value;
+      return this;
+    }
+
     MockPolicyCfg withMappedSearchBindPasswordProperty(final String value)
     {
       this.mappedSearchBindPasswordProperty = value;
@@ -566,6 +574,12 @@
     }
 
     @Override
+    public String getMappedSearchFilterTemplate()
+    {
+      return mappedSearchFilterTemplate;
+    }
+
+    @Override
     public long getCachedPasswordTTL()
     {
       return 86400;
@@ -1242,6 +1256,8 @@
         "sn: user",
         "cn: test user",
         "aduser: " + adDNString,
+        "samAccountName: aduser",
+        "customStatus: Active",
         "uid: aduser"
         /* @formatter:on */
     );
@@ -1821,6 +1837,11 @@
         { mockCfgWithPolicy(MAPPED_SEARCH).withMappedSearchBindPasswordFile("dummy_file.txt"), false },
         { mockCfgWithPolicy(MAPPED_SEARCH).withMappedSearchBindPasswordFile("config/admin-keystore.pin"), true },
 
+        { mockCfgWithPolicy(MAPPED_SEARCH).withMappedSearchFilterTemplate("invalidFilter"), false },
+        { mockCfgWithPolicy(MAPPED_SEARCH).withMappedSearchFilterTemplate("invalidFilter)"), false },
+        { mockCfgWithPolicy(MAPPED_SEARCH).withMappedSearchFilterTemplate("valid=filter"), true },
+        { mockCfgWithPolicy(MAPPED_SEARCH).withMappedSearchFilterTemplate("(valid=%s)"), true },
+
     };
     // @formatter:on
   }
@@ -2898,6 +2919,114 @@
   }
 
   /**
+   * Tests a mapped search with different filter templates.
+   * 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 filter
+   *          The mapping filter template
+   * @param bindResultCode
+   *          The bind result code.
+   * @throws Exception
+   *           If an unexpected exception occurred.
+   */
+  @Test(dataProvider = "testMappingFilterData")
+  public void testMappingFilterTemplateAuthentication(
+      final String filter, final ResultCode bindResultCode)
+      throws Exception
+  {
+    // Mock configuration.
+    final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
+        .withPrimaryServer(phost1)
+        .withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
+        .withMappedAttribute("uid")
+        .withMappedSearchFilterTemplate(filter)
+        .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 = new GetConnectionEvent(fe);
+
+    provider
+        .expectEvent(ceSearch)
+        .expectEvent(
+            new SimpleBindEvent(ceSearch, searchBindDNString,
+                "searchPassword", ResultCode.SUCCESS))
+        .expectEvent(
+            new SearchEvent(ceSearch, "o=ad", SearchScope.WHOLE_SUBTREE,
+                Filter.format(filter, "aduser").toString(), 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, 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}.
@@ -2924,6 +3053,26 @@
   }
 
   /**
+   * Returns test data for {@link #testMappingFilterTemplateAuthentication}.
+   *
+   * @return Test data for {@link #testMappingFilterTemplateAuthentication}.
+   */
+  @DataProvider
+  public Object[][] testMappingFilterData()
+  {
+    // @formatter:off
+    return new Object[][] {
+        /* policy, bind result code */
+        { "uid=%s", ResultCode.SUCCESS },
+        { "(&(uid=%s)(objectClass=nomatch))", ResultCode.INVALID_CREDENTIALS },
+        { "(samaccountname=%s)", ResultCode.SUCCESS },
+        { "(&(customstatus=Active)(samaccountname=%s))", ResultCode.SUCCESS },
+        { "filteris=notmatching", ResultCode.INVALID_CREDENTIALS },
+    };
+    // @formatter:on
+  }
+
+  /**
    * Tests that mapped PTA fails when no match attribute values are found.
    *
    * @throws Exception

--
Gitblit v1.10.0