mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

ian.packer
09.19.2016 d4262475f3e099e1023ba0e8c9ba53782268f52a
OPENDJ-1103 OPENDJ-1626 Allow configuration of a filter template for passthrough authentication mapped searches

Adds a new optional configuration property 'mapped-search-filter-template' which can be used to
override searches generated in the mapped search mode of operation.
5 files modified
232 ■■■■■ changed files
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/LDAPPassThroughAuthenticationPolicyConfiguration.xml 33 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/resource/schema/02-config.ldif 7 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java 41 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/extension.properties 2 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java 149 ●●●●● patch | view | raw | blame | history
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
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 $
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;
  }
}
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
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