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: "(&(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