From 5b7afcc00e450bf639610f27af7a1c3a3562a020 Mon Sep 17 00:00:00 2001
From: Yannick Lecaillez <yannick.lecaillez@forgerock.com>
Date: Thu, 19 May 2016 15:10:45 +0000
Subject: [PATCH] Rest2Ldap: Removed connection reuse, simplify authorization filtering, use factory methods, add more unit tests.
---
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SimpleBindStrategy.java | 11
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/ConditionalFilters.java | 130 +++++
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/CredentialExtractorsTest.java | 70 +++
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEndpoint.java | 29
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/DirectConnectionFilter.java | 2
opendj-server-legacy/resource/config/http-config.json | 12
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/HttpBasicAuthenticationFilterTest.java | 126 ++---
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java | 239 ++--------
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/CredentialExtractors.java | 132 ++++++
opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2FilterTest.java | 2
opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/InternalProxyAuthzFilter.java | 34
/dev/null | 65 --
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2Filter.java | 71 --
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SearchThenBindStrategy.java | 11
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AuthenticationStrategy.java | 9
opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json | 13
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/Authorizations.java | 100 ++++
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SASLPlainStrategy.java | 17
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AuthenticationStrategies.java | 98 ++++
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/HttpBasicAuthenticationFilter.java | 109 ----
20 files changed, 731 insertions(+), 549 deletions(-)
diff --git a/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json
index 830a126..54d9927 100644
--- a/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json
+++ b/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/classes/opendj-rest2ldap-config.json
@@ -78,12 +78,7 @@
"anonymous": {
// Specify the connection factory to use to perform LDAP operations.
// If missing, "root" factory will be used.
- "ldapConnectionFactory": "root",
-
- // Enable proxied authorization using the specified user DN.
- // If empty, anonymous proxied authorization will be used.
- // If missing, connection from the ldapConnectionFactory will be used as-is.
- "userDN": ""
+ "ldapConnectionFactory": "root"
},
// Use HTTP Basic authentication's information to bind to the LDAP server.
@@ -95,12 +90,6 @@
"altAuthenticationUsernameHeader" : "X-OpenIDM-Username",
"altAuthenticationPasswordHeader" : "X-OpenIDM-Password",
- // For server which are not supporting proxied-authorization control, you can
- // set this flag to true. Subsequent LDAP operations will then be performed
- // by re-using the authenticated connection.
- // If missing, proxied-authorization control will be used.
- "reuseAuthenticatedConnection": false,
-
// Define which LDAP bind mechanism to use
// Supported mechanisms are "simple", "sasl-plain", "search"
"bind": "search",
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java
index 41a3b08..986463d 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java
@@ -17,18 +17,22 @@
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.http.util.Json.readJsonLenient;
+import static org.forgerock.json.JsonValueFunctions.enumConstant;
+import static org.forgerock.json.JsonValueFunctions.setOf;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.configureConnectionFactory;
+import static org.forgerock.opendj.rest2ldap.authz.AuthenticationStrategies.*;
+import static org.forgerock.opendj.rest2ldap.authz.Authorizations.*;
+import static org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.*;
+import static org.forgerock.opendj.rest2ldap.authz.CredentialExtractors.*;
import static org.forgerock.util.Reject.checkNotNull;
import static org.forgerock.util.Utils.closeSilently;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
-import java.util.ArrayList;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
-import java.util.TreeMap;
+import java.util.Set;
import org.forgerock.http.Filter;
import org.forgerock.http.Handler;
@@ -40,6 +44,7 @@
import org.forgerock.http.protocol.Headers;
import org.forgerock.http.protocol.Request;
import org.forgerock.http.protocol.Response;
+import org.forgerock.http.protocol.Status;
import org.forgerock.json.JsonValue;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.Router;
@@ -47,21 +52,10 @@
import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.DN;
-import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.opendj.rest2ldap.authz.AuthenticationStrategy;
-import org.forgerock.opendj.rest2ldap.authz.DirectConnectionFilter;
-import org.forgerock.opendj.rest2ldap.authz.HttpBasicAuthenticationFilter;
-import org.forgerock.opendj.rest2ldap.authz.HttpBasicAuthenticationFilter.CustomHeaderExtractor;
-import org.forgerock.opendj.rest2ldap.authz.HttpBasicAuthenticationFilter.HttpBasicExtractor;
-import org.forgerock.opendj.rest2ldap.authz.OptionalFilter;
-import org.forgerock.opendj.rest2ldap.authz.OptionalFilter.ConditionalFilter;
-import org.forgerock.opendj.rest2ldap.authz.ProxiedAuthV2Filter;
-import org.forgerock.opendj.rest2ldap.authz.ProxiedAuthV2Filter.IntrospectionAuthzProvider;
-import org.forgerock.opendj.rest2ldap.authz.SASLPlainStrategy;
-import org.forgerock.opendj.rest2ldap.authz.SearchThenBindStrategy;
-import org.forgerock.opendj.rest2ldap.authz.SimpleBindStrategy;
+import org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.ConditionalFilter;
import org.forgerock.services.context.Context;
import org.forgerock.services.context.SecurityContext;
import org.forgerock.util.Factory;
@@ -87,21 +81,9 @@
private final Map<String, ConnectionFactory> connectionFactories = new HashMap<>();
- private enum Policy {
- oauth2 (0),
- basic (50),
- anonymous (100);
+ private enum Policy { BASIC, ANONYMOUS }
- private final int priority;
-
- Policy(int priority) {
- this.priority = priority;
- }
- }
-
- private enum BindStrategy {
- simple, search, sasl_plain
- }
+ private enum BindStrategy { SIMPLE, SEARCH, SASL_PLAIN }
/**
* Default constructor called by the HTTP Framework which will use the default configuration file location.
@@ -179,102 +161,57 @@
}
private Filter newAuthorizationFilter(final JsonValue config) {
- final List<Policy> configuredPolicies = new ArrayList<>();
- for (String policy : config.get("policies").required().asList(String.class)) {
- configuredPolicies.add(Policy.valueOf(policy.toLowerCase()));
- }
- final TreeMap<Integer, Filter> policyFilters = new TreeMap<>();
- final int lastIndex = configuredPolicies.size() - 1;
- for (int i = 0; i < configuredPolicies.size(); i++) {
- final Policy policy = configuredPolicies.get(i);
- policyFilters.put(policy.priority,
- buildAuthzPolicyFilter(policy, config.get(policy.toString()), i != lastIndex));
- }
- return Filters.chainOf(new ArrayList<>(policyFilters.values()));
- }
-
- private Filter buildAuthzPolicyFilter(final Policy policy, final JsonValue config, boolean optional) {
- switch (policy) {
- case anonymous:
- return buildAnonymousFilter(config);
- case basic:
- final ConditionalFilter basicFilter = buildBasicFilter(config.required());
- final Filter basicFilterChain =
- config.get("reuseAuthenticatedConnection").defaultTo(Boolean.FALSE).asBoolean()
- ? basicFilter
- : Filters.chainOf(basicFilter, newProxyAuthzFilter(getConnectionFactory(DEFAULT_ROOT_FACTORY),
- IntrospectionAuthzProvider.INSTANCE));
- return optional ? new OptionalFilter(basicFilterChain, basicFilter) : basicFilterChain;
- default:
- throw new IllegalArgumentException("Unsupported policy '" + policy + "'");
- }
- }
-
- /**
- * Create a new {@link Filter} in charge of injecting {@link AuthenticatedConnectionContext}.
- *
- * @param connectionFactory
- * The {@link ConnectionFactory} providing the {@link Connection} injected as
- * {@link AuthenticatedConnectionContext}
- * @param authzIdProvider
- * Function computing the authzId to use for the LDAP's ProxiedAuth control.
- * @return a newly created {@link Filter}
- */
- protected Filter newProxyAuthzFilter(final ConnectionFactory connectionFactory,
- final Function<SecurityContext, String, LdapException> authzIdProvider) {
- return new ProxiedAuthV2Filter(connectionFactory, authzIdProvider);
- }
-
- private Filter buildAnonymousFilter(final JsonValue config) {
- if (config.contains("userDN")) {
- final DN userDN = DN.valueOf(config.get("userDN").asString(), schema);
- final Map<String, Object> authz = new HashMap<>(1);
- authz.put(SecurityContext.AUTHZID_DN, userDN.toString());
- return Filters.chainOf(
- newStaticSecurityContextFilter(null, authz),
- newProxyAuthzFilter(
- getConnectionFactory(config.get("ldapConnectionFactory")
- .defaultTo(DEFAULT_ROOT_FACTORY)
- .asString()),
- IntrospectionAuthzProvider.INSTANCE));
- }
- return newDirectConnectionFilter(getConnectionFactory(config.get("ldapConnectionFactory")
- .defaultTo(DEFAULT_ROOT_FACTORY).asString()));
- }
-
- /**
- * Create a new {@link Filter} injecting a predefined {@link SecurityContext}.
- *
- * @param authenticationId
- * AuthenticationID of the {@link SecurityContext}.
- * @param authorization
- * Authorization of the {@link SecurityContext}
- * @return a newly created {@link Filter}
- */
- protected Filter newStaticSecurityContextFilter(final String authenticationId,
- final Map<String, Object> authorization) {
+ final Set<Policy> policies = config.get("policies").as(setOf(enumConstant(Policy.class)));
+ final ConditionalFilter anonymous =
+ policies.contains(Policy.ANONYMOUS) ? buildAnonymousFilter(config.get("anonymous")) : NEVER_APPLICABLE;
+ final ConditionalFilter basic =
+ policies.contains(Policy.BASIC) ? buildBasicFilter(config.get("basic")) : NEVER_APPLICABLE;
return new Filter() {
@Override
public Promise<Response, NeverThrowsException> filter(Context context, Request request, Handler next) {
- return next.handle(new SecurityContext(context, authenticationId, authorization), request);
+ if (basic.getCondition().canApplyFilter(context, request)) {
+ return basic.getFilter().filter(context, request, next);
+ }
+ if (anonymous.getCondition().canApplyFilter(context, request)) {
+ return anonymous.getFilter().filter(context, request, next);
+ }
+ return Response.newResponsePromise(new Response(Status.FORBIDDEN));
}
};
}
/**
- * Create a new {@link Filter} in charge of injecting {@link AuthenticatedConnectionContext} directly from a
+ * Creates a new {@link Filter} in charge of injecting {@link AuthenticatedConnectionContext}.
+ *
+ * @param connectionFactory
+ * The {@link ConnectionFactory} providing the {@link Connection} injected as
+ * {@link AuthenticatedConnectionContext}
+ * @return a newly created {@link Filter}
+ */
+ protected Filter newProxyAuthzFilter(final ConnectionFactory connectionFactory) {
+ return newProxyAuthorizationFilter(connectionFactory);
+ }
+
+ private ConditionalFilter buildAnonymousFilter(final JsonValue config) {
+ return newAnonymousFilter(getConnectionFactory(config.get("ldapConnectionFactory")
+ .defaultTo(DEFAULT_ROOT_FACTORY)
+ .asString()));
+ }
+
+ /**
+ * Creates a new {@link Filter} in charge of injecting {@link AuthenticatedConnectionContext} directly from a
* {@link ConnectionFactory}.
*
* @param connectionFactory
* The {@link ConnectionFactory} used to get the {@link Connection}
* @return a newly created {@link Filter}
*/
- protected Filter newDirectConnectionFilter(ConnectionFactory connectionFactory) {
- return new DirectConnectionFilter(connectionFactory);
+ protected ConditionalFilter newAnonymousFilter(ConnectionFactory connectionFactory) {
+ return newConditionalDirectConnectionFilter(connectionFactory);
}
/**
- * Get a {@link ConnectionFactory} from its name.
+ * Gets a {@link ConnectionFactory} from its name.
*
* @param name
* Name of the {@link ConnectionFactory} as specified in the configuration
@@ -286,42 +223,41 @@
private ConditionalFilter buildBasicFilter(final JsonValue config) {
final String bind = config.get("bind").required().asString();
- final BindStrategy strategy = BindStrategy.valueOf(bind.toLowerCase().replace('-', '_'));
+ final BindStrategy strategy = BindStrategy.valueOf(bind.toUpperCase().replace('-', '_'));
return newBasicAuthenticationFilter(buildBindStrategy(strategy, config.get(bind).required()),
config.get("supportAltAuthentication").defaultTo(Boolean.FALSE).asBoolean()
- ? new CustomHeaderExtractor(
+ ? newCustomHeaderExtractor(
config.get("altAuthenticationUsernameHeader").required().asString(),
config.get("altAuthenticationPasswordHeader").required().asString())
- : HttpBasicExtractor.INSTANCE,
- config.get("reuseAuthenticatedConnection").defaultTo(Boolean.FALSE).asBoolean());
+ : httpBasicExtractor());
}
/**
- * Get a {@link Filter} in charge of performing the HTTP-Basic Authentication. This filter create a
+ * Gets a {@link Filter} in charge of performing the HTTP-Basic Authentication. This filter create a
* {@link SecurityContext} reflecting the authenticated users.
*
* @param authenticationStrategy
* The {@link AuthenticationStrategy} to use to authenticate the user.
* @param credentialsExtractor
* Extract the user's credentials from the {@link Headers}.
- * @param reuseAuthenticatedConnection
- * Let the bound connection open so that it can be reused to perform the LDAP operations.
* @return A new {@link Filter}
*/
protected ConditionalFilter newBasicAuthenticationFilter(AuthenticationStrategy authenticationStrategy,
- Function<Headers, Pair<String, String>, NeverThrowsException> credentialsExtractor,
- boolean reuseAuthenticatedConnection) {
- return new HttpBasicAuthenticationFilter(authenticationStrategy, credentialsExtractor,
- reuseAuthenticatedConnection);
+ Function<Headers, Pair<String, String>, NeverThrowsException> credentialsExtractor) {
+ final ConditionalFilter httpBasicFilter =
+ newConditionalHttpBasicAuthenticationFilter(authenticationStrategy, credentialsExtractor);
+ return newConditionalFilter(Filters.chainOf(httpBasicFilter.getFilter(),
+ newProxyAuthzFilter(getConnectionFactory(DEFAULT_ROOT_FACTORY))),
+ httpBasicFilter.getCondition());
}
private AuthenticationStrategy buildBindStrategy(final BindStrategy strategy, final JsonValue config) {
switch (strategy) {
- case simple:
+ case SIMPLE:
return buildSimpleBindStrategy(config);
- case search:
+ case SEARCH:
return buildSearchThenBindStrategy(config);
- case sasl_plain:
+ case SASL_PLAIN:
return buildSASLBindStrategy(config);
default:
throw new IllegalArgumentException("Unsupported strategy '" + strategy + "'");
@@ -335,41 +271,10 @@
schema);
}
- /**
- * {@link AuthenticationStrategy} performing an LDAP Bind request with a computed DN.
- *
- * @param connectionFactory
- * The {@link ConnectionFactory} to use to perform the bind operation
- * @param schema
- * {@link Schema} used to perform the DN validation.
- * @param bindDNTemplate
- * DN template containing a single %s which will be replaced by the authenticating user's name. (i.e:
- * uid=%s,ou=people,dc=example,dc=com)
- * @return A new {@link AuthenticationStrategy}
- */
- protected AuthenticationStrategy newSimpleBindStrategy(ConnectionFactory connectionFactory, String bindDNTemplate,
- Schema schema) {
- return new SimpleBindStrategy(connectionFactory, bindDNTemplate, schema);
- }
-
private AuthenticationStrategy buildSASLBindStrategy(JsonValue config) {
- return newSASLBindStrategy(getConnectionFactory(config.get("ldapConnectionFactory")
- .defaultTo(DEFAULT_BIND_FACTORY).asString()),
- config.get("authcIdTemplate").defaultTo("u:%s").asString());
- }
-
- /**
- * {@link AuthenticationStrategy} performing an LDAP SASL-Plain Bind.
- *
- * @param connectionFactory
- * The {@link ConnectionFactory} to use to perform the bind operation
- * @param authcIdTemplate
- * Authentication identity template containing a single %s which will be replaced by the authenticating
- * user's name. (i.e: (u:%s)
- * @return A new {@link AuthenticationStrategy}
- */
- protected AuthenticationStrategy newSASLBindStrategy(ConnectionFactory connectionFactory, String authcIdTemplate) {
- return new SASLPlainStrategy(connectionFactory, schema, authcIdTemplate);
+ return newSASLPlainStrategy(
+ getConnectionFactory(config.get("ldapConnectionFactory").defaultTo(DEFAULT_BIND_FACTORY).asString()),
+ schema, config.get("authcIdTemplate").defaultTo("u:%s").asString());
}
private AuthenticationStrategy buildSearchThenBindStrategy(JsonValue config) {
@@ -382,26 +287,4 @@
SearchScope.valueOf(config.get("scope").required().asString().toLowerCase()),
config.get("filterTemplate").required().asString());
}
-
- /**
- * {@link AuthenticationStrategy} performing an LDAP Search to get a DN to bind with.
- *
- * @param searchConnectionFactory
- * The {@link ConnectionFactory} to sue to perform the search operation.
- * @param bindConnectionFactory
- * The {@link ConnectionFactory} to use to perform the bind operation
- * @param baseDN
- * The base DN of the search request
- * @param scope
- * {@link SearchScope} of the search request
- * @param filterTemplate
- * filter template containing a single %s which will be replaced by the authenticating user's name. (i.e:
- * (&(uid=%s)(objectClass=inetOrgPerson))
- * @return A new {@link AuthenticationStrategy}
- */
- protected AuthenticationStrategy newSearchThenBindStrategy(ConnectionFactory searchConnectionFactory,
- ConnectionFactory bindConnectionFactory, DN baseDN, SearchScope scope, String filterTemplate) {
- return new SearchThenBindStrategy(
- searchConnectionFactory, bindConnectionFactory, baseDN, scope, filterTemplate);
- }
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AuthenticationStrategies.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AuthenticationStrategies.java
new file mode 100644
index 0000000..b9f5acb
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AuthenticationStrategies.java
@@ -0,0 +1,98 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap.authz;
+
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.schema.Schema;
+
+
+/**
+ * Factory methods of {@link AuthenticationStrategy} allowing to perform authentication against LDAP server through
+ * different method.
+ */
+public final class AuthenticationStrategies {
+
+ private AuthenticationStrategies() {
+ }
+
+ /**
+ * Creates an {@link AuthenticationStrategy} performing simple BIND authentication against an LDAP server.
+ *
+ * @param connectionFactory
+ * {@link ConnectionFactory} to the LDAP server used to perform the bind operation.
+ * @param bindDNTemplate
+ * Tempalte of the DN to use for the bind operation. The first %s will be replaced by the provided
+ * authentication-id (i.e: uid=%s,dc=example,dc=com)
+ * @param schema
+ * {@link Schema} used to validate the DN format.*
+ * @return a new simple bind {@link AuthenticationStrategy}
+ * @throws NullPointerException
+ * If a parameter is null
+ */
+ public static AuthenticationStrategy newSimpleBindStrategy(ConnectionFactory connectionFactory,
+ String bindDNTemplate, Schema schema) {
+ return new SimpleBindStrategy(connectionFactory, bindDNTemplate, schema);
+ }
+
+ /**
+ * Creates an {@link AuthenticationStrategy} performing authentication against an LDAP server by first performing a
+ * lookup of the entry to bind with. This is to find the user DN to bind with from its metadata (i.e: email
+ * address).
+ *
+ * @param searchConnectionFactory
+ * {@link ConnectionFactory} to the LDAP server used to perform the lookup of the entry.
+ * @param bindConnectionFactory
+ * {@link ConnectionFactory} to the LDAP server used to perform the bind one the user's DN has been
+ * found. Can be the same than the searchConnectionFactory.
+ * @param baseDN
+ * Base DN of the search request performed to find the user's DN.
+ * @param searchScope
+ * {@link SearchScope} of the search request performed to find the user's DN.
+ * @param filterTemplate
+ * Filter of the search request (i.e: (&(email=%s)(objectClass=inetOrgPerson)) where the first %s will be
+ * replaced by the user's provided authentication-id.
+ * @return a new search then bind {@link AuthenticationStrategy}
+ * @throws NullPointerException
+ * If a parameter is null
+ */
+ public static AuthenticationStrategy newSearchThenBindStrategy(ConnectionFactory searchConnectionFactory,
+ ConnectionFactory bindConnectionFactory, DN baseDN, SearchScope searchScope, String filterTemplate) {
+ return new SearchThenBindStrategy(searchConnectionFactory, bindConnectionFactory, baseDN, searchScope,
+ filterTemplate);
+ }
+
+ /**
+ * Creates an {@link AuthenticationStrategy} performing authentication against an LDAP server using a plain SASL
+ * bind request.
+ *
+ * @param connectionFactory
+ * {@link ConnectionFactory} to the LDAP server to authenticate with.
+ * @param authcIdTemplate
+ * Authentication identity template containing a single %s which will be replaced by the authenticating
+ * user's name. (i.e: (u:%s)
+ * @param schema
+ * Schema used to perform DN validation.
+ * @return a new SASL plain bind {@link AuthenticationStrategy}
+ * @throws NullPointerException
+ * If a parameter is null
+ */
+ public static AuthenticationStrategy newSASLPlainStrategy(ConnectionFactory connectionFactory, Schema schema,
+ String authcIdTemplate) {
+ return new SASLPlainStrategy(connectionFactory, schema, authcIdTemplate);
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AuthenticationStrategy.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AuthenticationStrategy.java
index 9101def..a84d497 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AuthenticationStrategy.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/AuthenticationStrategy.java
@@ -15,9 +15,6 @@
*/
package org.forgerock.opendj.rest2ldap.authz;
-import java.util.concurrent.atomic.AtomicReference;
-
-import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.services.context.Context;
import org.forgerock.services.context.SecurityContext;
@@ -34,11 +31,7 @@
* Password used to perform the authentication.
* @param parentContext
* Context to use as parent for the created {@link SecurityContext}
- * @param authenticatedConnectionHolder
- * Output parameter. If supported, the implementations will set the reference to a ready to be used LDAP
- * connection bound to the given credentials.
* @return A {@link Context} if the authentication succeed or an {@link LdapException} otherwise.
*/
- Promise<SecurityContext, LdapException> authenticate(String username, String password, Context parentContext,
- AtomicReference<Connection> authenticatedConnectionHolder);
+ Promise<SecurityContext, LdapException> authenticate(String username, String password, Context parentContext);
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/Authorizations.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/Authorizations.java
new file mode 100644
index 0000000..471c03e
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/Authorizations.java
@@ -0,0 +1,100 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap.authz;
+
+import static org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.asConditionalFilter;
+import static org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.newConditionalFilter;
+
+import org.forgerock.http.Filter;
+import org.forgerock.http.protocol.Headers;
+import org.forgerock.http.protocol.Request;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.controls.ProxiedAuthV2RequestControl;
+import org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext;
+import org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.Condition;
+import org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.ConditionalFilter;
+import org.forgerock.services.context.Context;
+import org.forgerock.services.context.SecurityContext;
+import org.forgerock.util.Function;
+import org.forgerock.util.Pair;
+import org.forgerock.util.promise.NeverThrowsException;
+
+/**
+ * Factory methods to create {@link Filter} performing authentication and authorizations.
+ */
+public final class Authorizations {
+
+ private Authorizations() {
+ }
+
+ /**
+ * Creates a new {@link ConditionalFilter} performing authentication. If authentication succeed, it injects a
+ * {@link SecurityContext} with the authenticationId provided by the user. Otherwise, returns a HTTP 401 -
+ * Unauthorized response. The condition of this {@link ConditionalFilter} will return true if the supplied requests
+ * contains credentials information, false otherwise.
+ *
+ * @param authenticationStrategy
+ * {@link AuthenticationStrategy} to validate the user's provided credentials.
+ * @param credentialsExtractor
+ * Function to extract the credentials from the received request.
+ * @throws NullPointerException
+ * if a parameter is null.
+ * @return a new {@link ConditionalFilter}
+ */
+ public static ConditionalFilter newConditionalHttpBasicAuthenticationFilter(
+ final AuthenticationStrategy authenticationStrategy,
+ final Function<Headers, Pair<String, String>, NeverThrowsException> credentialsExtractor) {
+ return newConditionalFilter(
+ new HttpBasicAuthenticationFilter(authenticationStrategy, credentialsExtractor),
+ new Condition() {
+ @Override
+ public boolean canApplyFilter(Context context, Request request) {
+ return credentialsExtractor.apply(request.getHeaders()) != null;
+ }
+ });
+ }
+
+ /**
+ * Creates a {@link ConditionalFilter} injecting an {@link AuthenticatedConnectionContext} with a connection issued
+ * from the given connectionFactory. The condition is always true.
+ *
+ * @param connectionFactory
+ * The factory used to get the {@link Connection} to inject.
+ * @return A new {@link ConditionalFilter}.
+ * @throws NullPointerException
+ * if connectionFactory is null
+ */
+ public static ConditionalFilter newConditionalDirectConnectionFilter(ConnectionFactory connectionFactory) {
+ return asConditionalFilter(new DirectConnectionFilter(connectionFactory));
+ }
+
+ /**
+ * Creates a filter injecting an {@link AuthenticatedConnectionContext} given the information provided in the
+ * {@link SecurityContext}. The connection contained in the created {@link AuthenticatedConnectionContext} will add
+ * a {@link ProxiedAuthV2RequestControl} to each LDAP requests.
+ *
+ * @param connectionFactory
+ * The connection factory used to create the connection which will be injected in the
+ * {@link AuthenticatedConnectionContext}
+ * @return A new filter.
+ * @throws NullPointerException
+ * if connectionFactory is null
+ */
+ public static Filter newProxyAuthorizationFilter(ConnectionFactory connectionFactory) {
+ return new ProxiedAuthV2Filter(connectionFactory);
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/ConditionalFilters.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/ConditionalFilters.java
new file mode 100644
index 0000000..fa80e02
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/ConditionalFilters.java
@@ -0,0 +1,130 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.rest2ldap.authz;
+
+import static org.forgerock.util.Reject.checkNotNull;
+
+import org.forgerock.http.Filter;
+import org.forgerock.http.Handler;
+import org.forgerock.http.protocol.Request;
+import org.forgerock.http.protocol.Response;
+import org.forgerock.http.protocol.Status;
+import org.forgerock.services.context.Context;
+import org.forgerock.util.promise.NeverThrowsException;
+import org.forgerock.util.promise.Promise;
+
+/** Encapsulate a {@link Condition} which must be fulfilled in order to apply the Filter. */
+public final class ConditionalFilters {
+
+ /** Encapsulate a {@link Filter} which will be processed only if the attached {@link Condition} is true. */
+ public interface ConditionalFilter {
+ /**
+ * Get the filter which must be processed if the {@link Condition} evaluates to true.
+ *
+ * @return The filter to process.
+ */
+ Filter getFilter();
+
+ /**
+ * Get the {@link Condition} to evaluate.
+ *
+ * @return the {@link Condition} to evaluate.
+ */
+ Condition getCondition();
+ }
+
+ /** Condition which have to be fulfilled in order to apply the {@link Filter}. */
+ public interface Condition {
+ /**
+ * Check if a {@link Filter} must be executed or not.
+ *
+ * @param context
+ * Current {@link Context} of the request processing.
+ * @param request
+ * the {@link Request} currently processed.
+ * @return true if the filter must be applied.
+ */
+ boolean canApplyFilter(Context context, Request request);
+ }
+
+ /** {@link Condition} which always returns true. */
+ public static final Condition ALWAYS_TRUE = new Condition() {
+ @Override
+ public boolean canApplyFilter(Context context, Request request) {
+ return true;
+ }
+ };
+
+ /** {@link Condition} which always returns false. */
+ public static final Condition ALWAYS_FALSE = new Condition() {
+ @Override
+ public boolean canApplyFilter(Context context, Request request) {
+ return false;
+ }
+ };
+
+ /** {@link ConditionalFilter} with an ALWAYS_FALSE {@link Condition}. */
+ public static final ConditionalFilter NEVER_APPLICABLE = newConditionalFilter(new Filter() {
+ @Override
+ public Promise<Response, NeverThrowsException> filter(Context context, Request request, Handler next) {
+ return Response.newResponsePromise(new Response(Status.NOT_IMPLEMENTED));
+ }
+ }, ALWAYS_FALSE);
+
+ private ConditionalFilters() {
+ }
+
+ /**
+ * Wrap a {@link Filter} into a {@link ConditionalFilter} with an ALWAYS_TRUE condition.
+ *
+ * @param filter
+ * The {@link Filter} to wrap.
+ * @return a new {@link ConditionalFilter}
+ * @throws NullPointerException
+ * if filter is null
+ */
+ public static ConditionalFilter asConditionalFilter(final Filter filter) {
+ return newConditionalFilter(filter, ALWAYS_TRUE);
+ }
+
+ /**
+ * Create a {@link ConditionalFilter} from a {@link Filter} and a {@link Condition}.
+ *
+ * @param filter
+ * {@link Filter} which must be processed if the condition is true.
+ * @param condition
+ * {@link Condition} to evaluate.
+ * @return a new {@link ConditionalFilter}
+ * @throws NullPointerException
+ * if a parameter is null
+ */
+ public static ConditionalFilter newConditionalFilter(final Filter filter, final Condition condition) {
+ checkNotNull(filter, "filter cannot be null");
+ checkNotNull(condition, "condition cannot be null");
+ return new ConditionalFilter() {
+ @Override
+ public Filter getFilter() {
+ return filter;
+ }
+
+ @Override
+ public Condition getCondition() {
+ return condition;
+ }
+ };
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/CredentialExtractors.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/CredentialExtractors.java
new file mode 100644
index 0000000..50d5e9a
--- /dev/null
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/CredentialExtractors.java
@@ -0,0 +1,132 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap.authz;
+
+import static org.forgerock.util.Reject.checkNotNull;
+
+import org.forgerock.http.protocol.Headers;
+import org.forgerock.util.Function;
+import org.forgerock.util.Pair;
+import org.forgerock.util.encode.Base64;
+import org.forgerock.util.promise.NeverThrowsException;
+
+/**
+ * Factory method for function extracting credentials from HTTP request {@link Headers}.
+ */
+public final class CredentialExtractors {
+
+ /** HTTP Header sent by the client with HTTP basic authentication. */
+ public static final String HTTP_BASIC_AUTH_HEADER = "Authorization";
+
+ private CredentialExtractors() {
+ }
+
+ /**
+ * Creates a function which extracts the user's credentials from the standard HTTP Basic header.
+ *
+ * @return the basic extractor singleton
+ */
+ public static Function<Headers, Pair<String, String>, NeverThrowsException> httpBasicExtractor() {
+ return HttpBasicExtractor.INSTANCE;
+ }
+
+ /**
+ * Creates a function which extracts the user's credentials from custom HTTP header in addition of the standard HTTP
+ * Basic one.
+ *
+ * @param customHeaderUsername
+ * Name of the additional header to check for the user's name
+ * @param customHeaderPassword
+ * Name of the additional header to check for the user's password
+ * @return A new credentials extractors looking for custom header.
+ */
+ public static Function<Headers, Pair<String, String>, NeverThrowsException> newCustomHeaderExtractor(
+ String customHeaderUsername, String customHeaderPassword) {
+ return new CustomHeaderExtractor(customHeaderUsername, customHeaderPassword);
+ }
+
+ /** Extract the user's credentials from custom {@link Headers}. */
+ private static final class CustomHeaderExtractor
+ implements Function<Headers, Pair<String, String>, NeverThrowsException> {
+
+ private final String customHeaderUsername;
+ private final String customHeaderPassword;
+
+ /**
+ * Create a new CustomHeaderExtractor.
+ *
+ * @param customHeaderUsername
+ * Name of the header containing the username
+ * @param customHeaderPassword
+ * Name of the header containing the password
+ * @throws NullPointerException
+ * if a parameter is null.
+ */
+ public CustomHeaderExtractor(String customHeaderUsername, String customHeaderPassword) {
+ this.customHeaderUsername = checkNotNull(customHeaderUsername, "customHeaderUsername cannot be null");
+ this.customHeaderPassword = checkNotNull(customHeaderPassword, "customHeaderPassword cannot be null");
+ }
+
+ @Override
+ public Pair<String, String> apply(Headers headers) {
+ final String userName = headers.getFirst(customHeaderUsername);
+ final String password = headers.getFirst(customHeaderPassword);
+ if (userName != null && password != null) {
+ return Pair.of(userName, password);
+ }
+ return HttpBasicExtractor.INSTANCE.apply(headers);
+ }
+ }
+
+ /** Extract the user's credentials from the standard HTTP Basic {@link Headers}. */
+ private static final class HttpBasicExtractor
+ implements Function<Headers, Pair<String, String>, NeverThrowsException> {
+
+ /** Reference to the HttpBasicExtractor Singleton. */
+ public static final HttpBasicExtractor INSTANCE = new HttpBasicExtractor();
+
+ private HttpBasicExtractor() { }
+
+ @Override
+ public Pair<String, String> apply(Headers headers) {
+ final String httpBasicAuthHeader = headers.getFirst(HTTP_BASIC_AUTH_HEADER);
+ if (httpBasicAuthHeader != null) {
+ final Pair<String, String> userCredentials = parseUsernamePassword(httpBasicAuthHeader);
+ if (userCredentials != null) {
+ return userCredentials;
+ }
+ }
+ return null;
+ }
+
+ private Pair<String, String> parseUsernamePassword(String authHeader) {
+ if (authHeader != null && (authHeader.toLowerCase().startsWith("basic"))) {
+ // We received authentication info
+ // Example received header:
+ // "Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="
+ final String base64UserCredentials = authHeader.substring("basic".length() + 1);
+ // Example usage of base64:
+ // Base64("Aladdin:open sesame") = "QWxhZGRpbjpvcGVuIHNlc2FtZQ=="
+ final String userCredentials = new String(Base64.decode(base64UserCredentials));
+ String[] split = userCredentials.split(":");
+ if (split.length == 2) {
+ return Pair.of(split[0], split[1]);
+ }
+ }
+ return null;
+ }
+ }
+}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/DirectConnectionFilter.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/DirectConnectionFilter.java
index ecb187a..f3dcceb 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/DirectConnectionFilter.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/DirectConnectionFilter.java
@@ -36,7 +36,7 @@
/**
* Inject {@link Connection} into a {@link AuthenticatedConnectionContext}.
*/
-public final class DirectConnectionFilter implements Filter {
+final class DirectConnectionFilter implements Filter {
private final ConnectionFactory connectionFactory;
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/HttpBasicAuthenticationFilter.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/HttpBasicAuthenticationFilter.java
index 43c1aa8..0a53865 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/HttpBasicAuthenticationFilter.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/HttpBasicAuthenticationFilter.java
@@ -17,26 +17,20 @@
import static org.forgerock.opendj.rest2ldap.authz.Utils.asErrorResponse;
import static org.forgerock.util.Reject.checkNotNull;
-import static org.forgerock.opendj.rest2ldap.authz.Utils.close;
-import java.util.concurrent.atomic.AtomicReference;
-
+import org.forgerock.http.Filter;
import org.forgerock.http.Handler;
import org.forgerock.http.protocol.Headers;
import org.forgerock.http.protocol.Request;
import org.forgerock.http.protocol.Response;
-import org.forgerock.opendj.ldap.Connection;
import org.forgerock.opendj.ldap.EntryNotFoundException;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.ResultCode;
-import org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext;
-import org.forgerock.opendj.rest2ldap.authz.OptionalFilter.ConditionalFilter;
import org.forgerock.services.context.Context;
import org.forgerock.services.context.SecurityContext;
import org.forgerock.util.AsyncFunction;
import org.forgerock.util.Function;
import org.forgerock.util.Pair;
-import org.forgerock.util.encode.Base64;
import org.forgerock.util.promise.NeverThrowsException;
import org.forgerock.util.promise.Promise;
@@ -44,11 +38,10 @@
* Inject a {@link SecurityContext} if the credentials provided in the {@link Request} headers have been successfully
* verified.
*/
-public final class HttpBasicAuthenticationFilter implements ConditionalFilter {
+final class HttpBasicAuthenticationFilter implements Filter {
private final AuthenticationStrategy authenticationStrategy;
private final Function<Headers, Pair<String, String>, NeverThrowsException> credentialsExtractor;
- private final boolean reuseAuthenticatedConnection;
/**
* Create a new HttpBasicAuthenticationFilter.
@@ -57,22 +50,13 @@
* The strategy to use to perform the authentication.
* @param credentialsExtractor
* The function to use to extract credentials from the {@link Headers}.
- * @param reuseAuthenticatedConnection
- * Let the bound connection open so that it can be reused to perform the LDAP operations.
* @throws NullPointerException
* If a parameter is null.
*/
public HttpBasicAuthenticationFilter(AuthenticationStrategy authenticationStrategy,
- Function<Headers, Pair<String, String>, NeverThrowsException> credentialsExtractor,
- boolean reuseAuthenticatedConnection) {
+ Function<Headers, Pair<String, String>, NeverThrowsException> credentialsExtractor) {
this.authenticationStrategy = checkNotNull(authenticationStrategy, "authenticationStrategy cannot be null");
this.credentialsExtractor = checkNotNull(credentialsExtractor, "credentialsExtractor cannot be null");
- this.reuseAuthenticatedConnection = reuseAuthenticatedConnection;
- }
-
- @Override
- public boolean canApplyFilter(Context context, Request request) {
- return credentialsExtractor.apply(request.getHeaders()) != null;
}
@Override
@@ -82,18 +66,11 @@
if (credentials == null) {
return asErrorResponse(LdapException.newLdapException(ResultCode.INVALID_CREDENTIALS));
}
- final AtomicReference<Connection> authConnHolder = new AtomicReference<Connection>();
return authenticationStrategy
- .authenticate(credentials.getFirst(), credentials.getSecond(), context, authConnHolder)
+ .authenticate(credentials.getFirst(), credentials.getSecond(), context)
.thenAsync(new AsyncFunction<SecurityContext, Response, NeverThrowsException>() {
@Override
public Promise<Response, NeverThrowsException> apply(final SecurityContext securityContext) {
- if (reuseAuthenticatedConnection) {
- return next
- .handle(new AuthenticatedConnectionContext(securityContext, authConnHolder.get()),
- request);
- }
- close(authConnHolder);
return next.handle(securityContext, request);
}
}, new AsyncFunction<LdapException, Response, NeverThrowsException>() {
@@ -103,82 +80,6 @@
? LdapException.newLdapException(ResultCode.INVALID_CREDENTIALS, exception.getMessage())
: exception);
}
- })
- .thenFinally(close(authConnHolder));
- }
-
- /** Extract the user's credentials from custom {@link Headers}. */
- public static final class CustomHeaderExtractor
- implements Function<Headers, Pair<String, String>, NeverThrowsException> {
-
- private final String customHeaderUsername;
- private final String customHeaderPassword;
-
- /**
- * Create a new CustomHeaderExtractor.
- *
- * @param customHeaderUsername
- * Name of the header containing the username
- * @param customHeaderPassword
- * Name of the header containing the password
- * @throws NullPointerException
- * if a parameter is null.
- */
- public CustomHeaderExtractor(String customHeaderUsername, String customHeaderPassword) {
- this.customHeaderUsername = checkNotNull(customHeaderUsername, "customHeaderUsername cannot be null");
- this.customHeaderPassword = checkNotNull(customHeaderPassword, "customHeaderPassword cannot be null");
- }
-
- @Override
- public Pair<String, String> apply(Headers headers) {
- final String userName = headers.getFirst(customHeaderUsername);
- final String password = headers.getFirst(customHeaderPassword);
- if (userName != null && password != null) {
- return Pair.of(userName, password);
- }
- return HttpBasicExtractor.INSTANCE.apply(headers);
- }
- }
-
- /** Extract the user's credentials from the standard HTTP Basic {@link Headers}. */
- public static final class HttpBasicExtractor
- implements Function<Headers, Pair<String, String>, NeverThrowsException> {
-
- /** HTTP Header sent by the client with HTTP basic authentication. */
- public static final String HTTP_BASIC_AUTH_HEADER = "Authorization";
-
- /** Reference to the HttpBasicExtractor Singleton. */
- public static final HttpBasicExtractor INSTANCE = new HttpBasicExtractor();
-
- private HttpBasicExtractor() { }
-
- @Override
- public Pair<String, String> apply(Headers headers) {
- final String httpBasicAuthHeader = headers.getFirst(HTTP_BASIC_AUTH_HEADER);
- if (httpBasicAuthHeader != null) {
- final Pair<String, String> userCredentials = parseUsernamePassword(httpBasicAuthHeader);
- if (userCredentials != null) {
- return userCredentials;
- }
- }
- return null;
- }
-
- private Pair<String, String> parseUsernamePassword(String authHeader) {
- if (authHeader != null && (authHeader.toLowerCase().startsWith("basic"))) {
- // We received authentication info
- // Example received header:
- // "Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="
- final String base64UserCredentials = authHeader.substring("basic".length() + 1);
- // Example usage of base64:
- // Base64("Aladdin:open sesame") = "QWxhZGRpbjpvcGVuIHNlc2FtZQ=="
- final String userCredentials = new String(Base64.decode(base64UserCredentials));
- String[] split = userCredentials.split(":");
- if (split.length == 2) {
- return Pair.of(split[0], split[1]);
- }
- }
- return null;
- }
+ });
}
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/OptionalFilter.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/OptionalFilter.java
deleted file mode 100644
index cc448ba..0000000
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/OptionalFilter.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * The contents of this file are subject to the terms of the Common Development and
- * Distribution License (the License). You may not use this file except in compliance with the
- * License.
- *
- * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
- * specific language governing permission and limitations under the License.
- *
- * When distributing Covered Software, include this CDDL Header Notice in each file and include
- * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
- * Header, with the fields enclosed by brackets [] replaced by your own identifying
- * information: "Portions copyright [year] [name of copyright owner]".
- *
- * Copyright 2016 ForgeRock AS.
- */
-
-package org.forgerock.opendj.rest2ldap.authz;
-
-import org.forgerock.http.Filter;
-import org.forgerock.http.Handler;
-import org.forgerock.http.protocol.Request;
-import org.forgerock.http.protocol.Response;
-import org.forgerock.services.context.Context;
-import org.forgerock.util.promise.NeverThrowsException;
-import org.forgerock.util.promise.Promise;
-
-/** Encapsulate a {@link Filter} which can be conditionally applied. */
-public final class OptionalFilter implements Filter {
-
- /** Condition which have to be fulfilled in order to apply the {@link Filter}. */
- public interface Condition {
- /**
- * Check if a {@link Filter} must be executed or not.
- *
- * @param context
- * Current {@link Context} of the request processing.
- * @param request
- * the {@link Request} currently processed.
- * @return true if the filter must be applied.
- */
- boolean canApplyFilter(Context context, Request request);
- }
-
- /** A {@link Filter} which can be conditionally applied. */
- public interface ConditionalFilter extends Filter, Condition {
- }
-
- private final Filter delegate;
- private final Condition condition;
-
- /**
- * Make {@link Filter} optional.
- *
- * @param delegate
- * {@link Filter} which will be conditionally applied;
- * @param condition
- * The {@link Condition} which have to be fulfilled in order to apply the filter.
- */
- public OptionalFilter(Filter delegate, Condition condition) {
- this.delegate = delegate;
- this.condition = condition;
- }
-
- @Override
- public Promise<Response, NeverThrowsException> filter(Context context, Request request, Handler next) {
- if (condition.canApplyFilter(context, request)) {
- return delegate.filter(context, request, next);
- }
- return next.handle(context, request);
- }
-}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2Filter.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2Filter.java
index 7cd316c..db76fb9 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2Filter.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2Filter.java
@@ -47,7 +47,6 @@
import org.forgerock.opendj.ldap.responses.CompareResult;
import org.forgerock.opendj.ldap.responses.ExtendedResult;
import org.forgerock.opendj.ldap.responses.Result;
-import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext;
import org.forgerock.services.context.Context;
import org.forgerock.services.context.SecurityContext;
@@ -60,10 +59,9 @@
* Inject an {@link AuthenticatedConnectionContext} following the information provided in the {@link SecurityContext}.
* This connection will add a {@link ProxiedAuthV2RequestControl} to each LDAP requests.
*/
-public final class ProxiedAuthV2Filter implements Filter {
+final class ProxiedAuthV2Filter implements Filter {
private final ConnectionFactory connectionFactory;
- private final Function<SecurityContext, String, LdapException> authzProvider;
/**
* Create a new ProxyAuthzFilter. The {@link Connection} contained in the injected
@@ -78,10 +76,8 @@
* @throws NullPointerException
* If a parameter is null
*/
- public ProxiedAuthV2Filter(final ConnectionFactory connectionFactory,
- final Function<SecurityContext, String, LdapException> authzIdProvider) {
+ public ProxiedAuthV2Filter(final ConnectionFactory connectionFactory) {
this.connectionFactory = checkNotNull(connectionFactory, "connectionFactory cannot be null");
- this.authzProvider = checkNotNull(authzIdProvider, "authzIdProvider cannot be null");
}
@Override
@@ -95,7 +91,7 @@
public Connection apply(Connection connection) throws LdapException {
connectionHolder.set(connection);
final Connection proxiedConnection = newProxiedConnection(
- connection, authzProvider.apply(context.asContext(SecurityContext.class)));
+ connection, resolveAuthorizationId(context.asContext(SecurityContext.class)));
connectionHolder.set(proxiedConnection);
return proxiedConnection;
}
@@ -114,59 +110,24 @@
.thenFinally(close(connectionHolder));
}
+ private String resolveAuthorizationId(SecurityContext securityContext) throws LdapException {
+ Object candidate;
+ candidate = securityContext.getAuthorization().get(AUTHZID_DN);
+ if (candidate != null) {
+ return "dn:" + candidate;
+ }
+ candidate = securityContext.getAuthorization().get(AUTHZID_ID);
+ if (candidate != null) {
+ return "u:" + candidate;
+ }
+ throw LdapException.newLdapException(ResultCode.AUTH_METHOD_NOT_SUPPORTED);
+ }
+
private Connection newProxiedConnection(Connection baseConnection, String authzId) {
return new CachedReadConnectionDecorator(
new ProxiedAuthConnectionDecorator(baseConnection, newControl(authzId)));
}
- /**
- * Introspect the content of the {@link SecurityContext} and return the contained DN, or the user's ID if DN is not
- * present.
- */
- public static final class IntrospectionAuthzProvider implements Function<SecurityContext, String, LdapException> {
-
- /** Singleton instance. */
- public static final Function<SecurityContext, String, LdapException> INSTANCE =
- new IntrospectionAuthzProvider();
-
- private IntrospectionAuthzProvider() {
- }
-
- @Override
- public String apply(SecurityContext securityContext) throws LdapException {
- Object candidate;
- candidate = securityContext.getAuthorization().get(AUTHZID_DN);
- if (candidate != null) {
- return "dn:" + candidate;
- }
- candidate = securityContext.getAuthorization().get(AUTHZID_ID);
- if (candidate != null) {
- return "u:" + candidate;
- }
- throw LdapException.newLdapException(ResultCode.AUTH_METHOD_NOT_SUPPORTED);
- }
- }
-
- /** Use a {@link AuthzIdTemplate} to compute the authzId from the provided {@link SecurityContext}. */
- public static final class TemplateAuthzProvider implements Function<SecurityContext, String, LdapException> {
- private final AuthzIdTemplate template;
- private final Schema schema;
-
- TemplateAuthzProvider(AuthzIdTemplate template, Schema schema) {
- this.template = checkNotNull(template, "template cannot be null");
- this.schema = checkNotNull(schema, "schema cannot be null");
- }
-
- @Override
- public String apply(SecurityContext securityContext) throws LdapException {
- try {
- return template.formatAsAuthzId(securityContext.getAuthorization(), schema);
- } catch (IllegalArgumentException e) {
- throw LdapException.newLdapException(ResultCode.OPERATIONS_ERROR);
- }
- }
- }
-
private static final class ProxiedAuthConnectionDecorator extends AbstractAsynchronousConnectionDecorator {
private final Control proxiedAuthzControl;
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SASLPlainStrategy.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SASLPlainStrategy.java
index 11d8e25..b3c9750 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SASLPlainStrategy.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SASLPlainStrategy.java
@@ -19,6 +19,7 @@
import static org.forgerock.services.context.SecurityContext.AUTHZID_DN;
import static org.forgerock.services.context.SecurityContext.AUTHZID_ID;
import static org.forgerock.util.Reject.checkNotNull;
+import static org.forgerock.opendj.rest2ldap.authz.Utils.close;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -42,9 +43,8 @@
import org.forgerock.util.Function;
import org.forgerock.util.promise.Promise;
-
/** Bind using a computed DN from a template and the current request/context. */
-public final class SASLPlainStrategy implements AuthenticationStrategy {
+final class SASLPlainStrategy implements AuthenticationStrategy {
private final ConnectionFactory connectionFactory;
private final Function<String, String, LdapException> formatter;
@@ -90,21 +90,22 @@
@Override
public Promise<SecurityContext, LdapException> authenticate(final String username, final String password,
- final Context parentContext, final AtomicReference<Connection> authenticateConnectionHolder) {
+ final Context parentContext) {
+ final AtomicReference<Connection> connectionHolder = new AtomicReference<Connection>();
return connectionFactory
.getConnectionAsync()
.thenAsync(new AsyncFunction<Connection, SecurityContext, LdapException>() {
@Override
public Promise<SecurityContext, LdapException> apply(Connection connection) throws LdapException {
- authenticateConnectionHolder.set(connection);
- final String authcId = formatter.apply(username);
- return doSASLPlainBind(connection, parentContext, username, authcId, password);
+ connectionHolder.set(connection);
+ return doSASLPlainBind(connection, parentContext, username, password);
}
- });
+ }).thenFinally(close(connectionHolder));
}
private Promise<SecurityContext, LdapException> doSASLPlainBind(final Connection connection,
- final Context parentContext, final String authzId, final String authcId, final String password) {
+ final Context parentContext, final String authzId, final String password) throws LdapException {
+ final String authcId = formatter.apply(authzId);
return connection
.bindAsync(newPlainSASLBindRequest(authcId, password.toCharArray())
.addControl(AuthorizationIdentityRequestControl.newControl(true)))
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SearchThenBindStrategy.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SearchThenBindStrategy.java
index c0f1d00..c2a1d76 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SearchThenBindStrategy.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SearchThenBindStrategy.java
@@ -35,7 +35,7 @@
import org.forgerock.util.promise.Promise;
/** Bind using the result of a search request computed from the current request/context. */
-public final class SearchThenBindStrategy implements AuthenticationStrategy {
+final class SearchThenBindStrategy implements AuthenticationStrategy {
private final ConnectionFactory searchConnectionFactory;
private final ConnectionFactory bindConnectionFactory;
@@ -70,7 +70,7 @@
@Override
public Promise<SecurityContext, LdapException> authenticate(final String username, final String password,
- final Context parentContext, final AtomicReference<Connection> authenticatedConnectionHolder) {
+ final Context parentContext) {
final AtomicReference<Connection> searchConnectionHolder = new AtomicReference<>();
return searchConnectionFactory
.getConnectionAsync()
@@ -90,11 +90,12 @@
@Override
public Promise<SecurityContext, LdapException> apply(final SearchResultEntry searchResult)
throws LdapException {
+ final AtomicReference<Connection> bindConnectionHolder = new AtomicReference<>();
return bindConnectionFactory
.getConnectionAsync()
- .thenAsync(
- doSimpleBind(authenticatedConnectionHolder, parentContext, username,
- searchResult.getName(), password));
+ .thenAsync(doSimpleBind(bindConnectionHolder, parentContext, username,
+ searchResult.getName(), password))
+ .thenFinally(close(bindConnectionHolder));
}
});
}
diff --git a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SimpleBindStrategy.java b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SimpleBindStrategy.java
index 4d77253..dc4dcd3 100644
--- a/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SimpleBindStrategy.java
+++ b/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/authz/SimpleBindStrategy.java
@@ -16,6 +16,7 @@
package org.forgerock.opendj.rest2ldap.authz;
import static org.forgerock.opendj.ldap.requests.Requests.newSimpleBindRequest;
+import static org.forgerock.opendj.rest2ldap.authz.Utils.close;
import static org.forgerock.services.context.SecurityContext.AUTHZID_DN;
import static org.forgerock.services.context.SecurityContext.AUTHZID_ID;
import static org.forgerock.util.Reject.checkNotNull;
@@ -37,7 +38,7 @@
import org.forgerock.util.promise.Promise;
/** Bind using a computed DN from a template and the current request/context. */
-public final class SimpleBindStrategy implements AuthenticationStrategy {
+final class SimpleBindStrategy implements AuthenticationStrategy {
private final ConnectionFactory connectionFactory;
private final Schema schema;
@@ -64,11 +65,13 @@
@Override
public Promise<SecurityContext, LdapException> authenticate(final String username, final String password,
- final Context parentContext, final AtomicReference<Connection> authenticatedConnectionHolder) {
+ final Context parentContext) {
+ final AtomicReference<Connection> connectionHolder = new AtomicReference<>();
return connectionFactory
.getConnectionAsync()
- .thenAsync(doSimpleBind(authenticatedConnectionHolder, parentContext, username,
- DN.format(bindDNTemplate, schema, username), password));
+ .thenAsync(doSimpleBind(connectionHolder, parentContext, username,
+ DN.format(bindDNTemplate, schema, username), password))
+ .thenFinally(close(connectionHolder));
}
static AsyncFunction<Connection, SecurityContext, LdapException> doSimpleBind(
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/CredentialExtractorsTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/CredentialExtractorsTest.java
new file mode 100644
index 0000000..54e0209
--- /dev/null
+++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/CredentialExtractorsTest.java
@@ -0,0 +1,70 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.forgerock.opendj.rest2ldap.authz;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.forgerock.opendj.rest2ldap.authz.CredentialExtractors.HTTP_BASIC_AUTH_HEADER;
+import static org.forgerock.opendj.rest2ldap.authz.CredentialExtractors.httpBasicExtractor;
+import static org.forgerock.opendj.rest2ldap.authz.CredentialExtractors.newCustomHeaderExtractor;
+
+import org.forgerock.http.protocol.Headers;
+import org.forgerock.testng.ForgeRockTestCase;
+import org.forgerock.util.Pair;
+import org.forgerock.util.encode.Base64;
+import org.testng.annotations.Test;
+
+@Test
+public class CredentialExtractorsTest extends ForgeRockTestCase {
+
+ @Test
+ public void testBasicCanExtractValidCredentials() {
+ final Headers headers = new Headers();
+ headers.put(HTTP_BASIC_AUTH_HEADER, "basic " + Base64.encode("foo:bar".getBytes()));
+ assertThat(httpBasicExtractor().apply(headers)).isEqualTo(Pair.of("foo", "bar"));
+ }
+
+ @Test
+ public void testBasicReturnNullOnInvalidCredentials() {
+ final Headers headers = new Headers();
+ headers.put(HTTP_BASIC_AUTH_HEADER, "*invalid*");
+ assertThat(httpBasicExtractor().apply(new Headers())).isNull();
+ }
+
+ @Test
+ public void testBasicReturnNullOnMissingCredentials() {
+ assertThat(httpBasicExtractor().apply(new Headers())).isNull();
+ }
+
+ @Test
+ public void testCustomCanExtractValidCredentials() {
+ final Headers headers = new Headers();
+ headers.put("X-user", "foo");
+ headers.put("X-password", "bar");
+ assertThat(newCustomHeaderExtractor("X-user", "X-password").apply(headers)).isEqualTo(Pair.of("foo", "bar"));
+ }
+
+ @Test
+ public void testCustomFallbackOnBasicIfMissingCustomCredentials() {
+ final Headers headers = new Headers();
+ headers.put(HTTP_BASIC_AUTH_HEADER, "basic " + Base64.encode("foo:bar".getBytes()));
+ assertThat(newCustomHeaderExtractor("X-user", "X-password").apply(headers)).isEqualTo(Pair.of("foo", "bar"));
+ }
+
+ @Test
+ public void testCustomReturnNullOnMissingCredentials() {
+ assertThat(httpBasicExtractor().apply(new Headers())).isNull();
+ }
+}
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/HttpBasicAuthenticationFilterTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/HttpBasicAuthenticationFilterTest.java
index a77a9a9..cc0e877 100644
--- a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/HttpBasicAuthenticationFilterTest.java
+++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/HttpBasicAuthenticationFilterTest.java
@@ -15,13 +15,17 @@
*/
package org.forgerock.opendj.rest2ldap.authz;
-import static org.fest.assertions.Assertions.*;
-import static org.forgerock.opendj.rest2ldap.authz.HttpBasicAuthenticationFilter.HttpBasicExtractor.*;
-import static org.mockito.Matchers.*;
-import static org.mockito.Mockito.*;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.forgerock.opendj.rest2ldap.authz.Authorizations.newConditionalHttpBasicAuthenticationFilter;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
import java.io.IOException;
-import java.util.concurrent.atomic.AtomicReference;
+import java.util.Collections;
+import java.util.concurrent.ExecutionException;
import org.assertj.core.api.SoftAssertions;
import org.forgerock.http.Handler;
@@ -29,67 +33,69 @@
import org.forgerock.http.protocol.Request;
import org.forgerock.http.protocol.Response;
import org.forgerock.opendj.ldap.LdapException;
-import org.forgerock.opendj.rest2ldap.authz.HttpBasicAuthenticationFilter.CustomHeaderExtractor;
-import org.forgerock.opendj.rest2ldap.authz.HttpBasicAuthenticationFilter.HttpBasicExtractor;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.ConditionalFilter;
import org.forgerock.services.context.Context;
+import org.forgerock.services.context.RootContext;
import org.forgerock.services.context.SecurityContext;
import org.forgerock.testng.ForgeRockTestCase;
+import org.forgerock.util.Function;
import org.forgerock.util.Pair;
-import org.forgerock.util.encode.Base64;
+import org.forgerock.util.promise.NeverThrowsException;
import org.forgerock.util.promise.Promises;
-import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@Test
public class HttpBasicAuthenticationFilterTest extends ForgeRockTestCase {
- private static final String USERNAME = "Aladdin";
- private static final String PASSWORD = "open sesame";
- private static final String BASE64_USERPASS = Base64.encode((USERNAME + ":" + PASSWORD).getBytes());
-
- @DataProvider(name = "Invalid HTTP basic auth strings")
- public Object[][] getInvalidHttpBasicAuthStrings() {
- return new Object[][] { { null }, { "bla" }, { "basic " + Base64.encode("la:bli:blu".getBytes()) } };
- }
-
- @Test(dataProvider = "Invalid HTTP basic auth strings")
- public void parseUsernamePasswordFromInvalidAuthZHeader(String authZHeader) throws Exception {
- final AuthenticationStrategy strategy = mock(AuthenticationStrategy.class);
- final HttpBasicAuthenticationFilter filter =
- new HttpBasicAuthenticationFilter(strategy, HttpBasicExtractor.INSTANCE, false);
-
- final Request req = new Request();
- req.getHeaders().put(HTTP_BASIC_AUTH_HEADER, authZHeader);
-
- assertThat(filter.canApplyFilter(null, req)).isFalse();
- }
-
- @DataProvider(name = "Valid HTTP basic auth strings")
- public Object[][] getValidHttpBasicAuthStrings() {
- return new Object[][] { { "basic " + BASE64_USERPASS }, { "Basic " + BASE64_USERPASS } };
- }
-
- @Test(dataProvider = "Valid HTTP basic auth strings")
- public void parseUsernamePasswordFromValidAuthZHeader(String authZHeader) throws Exception {
- final Headers headers = new Headers();
- headers.put(HTTP_BASIC_AUTH_HEADER, authZHeader);
- assertThat(HttpBasicExtractor.INSTANCE.apply(headers)).isEqualTo(Pair.of(USERNAME, PASSWORD));
- }
-
+ @SuppressWarnings("unchecked")
@Test
- public void sendUnauthorizedResponseWithHttpBasicAuthWillChallengeUserAgent() throws Exception {
- final AuthenticationStrategy failureStrategy = mock(AuthenticationStrategy.class);
- when(failureStrategy
- .authenticate(any(String.class), any(String.class), any(Context.class), any(AtomicReference.class)))
- .thenReturn(Promises.<SecurityContext, LdapException>newResultPromise(null));
+ public void testRespondUnauthorizedIfCredentialMissing()
+ throws InterruptedException, ExecutionException, IOException {
+ final ConditionalFilter filter = newConditionalHttpBasicAuthenticationFilter(mock(AuthenticationStrategy.class),
+ mock(Function.class));
- final HttpBasicAuthenticationFilter filter =
- new HttpBasicAuthenticationFilter(failureStrategy, HttpBasicExtractor.INSTANCE, false);
+ assertThat(filter.getCondition().canApplyFilter(new RootContext(), new Request())).isFalse();
+ verifyUnauthorizedOutputMessage(
+ filter.getFilter().filter(mock(Context.class), new Request(), mock(Handler.class)).get());
+ }
- final Response response = filter.filter(null, new Request(), mock(Handler.class)).get();
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testRespondUnauthorizedIfCredentialWrong()
+ throws InterruptedException, ExecutionException, IOException {
+ final Function<Headers, Pair<String, String>, NeverThrowsException> credentials = mock(Function.class);
+ when(credentials.apply(any(Headers.class))).thenReturn(Pair.of("user", "password"));
+
+ final AuthenticationStrategy authStrategy = mock(AuthenticationStrategy.class);
+ when(authStrategy.authenticate(eq("user"), eq("password"), any(Context.class)))
+ .thenReturn(Promises.<SecurityContext, LdapException> newExceptionPromise(
+ LdapException.newLdapException(ResultCode.INVALID_CREDENTIALS)));
+
+ final Response response = new HttpBasicAuthenticationFilter(authStrategy, credentials)
+ .filter(mock(Context.class), new Request(), mock(Handler.class)).get();
+
verifyUnauthorizedOutputMessage(response);
}
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testContinueProcessOnSuccessfullAuthentication() {
+ final Function<Headers, Pair<String, String>, NeverThrowsException> credentials = mock(Function.class);
+ when(credentials.apply(any(Headers.class))).thenReturn(Pair.of("user", "password"));
+
+ final AuthenticationStrategy authStrategy = mock(AuthenticationStrategy.class);
+ when(authStrategy.authenticate(eq("user"), eq("password"), any(Context.class)))
+ .thenReturn(Promises.<SecurityContext, LdapException> newResultPromise(
+ new SecurityContext(new RootContext(), "user", Collections.<String, Object> emptyMap())));
+
+ final Handler handler = mock(Handler.class);
+ new HttpBasicAuthenticationFilter(authStrategy, credentials)
+ .filter(mock(Context.class), new Request(), handler);
+
+ verify(handler).handle(any(SecurityContext.class), any(Request.class));
+ }
+
private void verifyUnauthorizedOutputMessage(Response response) throws IOException {
final SoftAssertions softly = new SoftAssertions();
softly.assertThat(response.getStatus().getCode()).isEqualTo(401);
@@ -98,24 +104,4 @@
.isEqualTo("{code=401, reason=Unauthorized, message=Invalid Credentials}");
softly.assertAll();
}
-
- @Test
- public void extractUsernamePasswordHttpBasicAuthWillAcceptUserAgent() throws Exception {
- final Headers headers = new Headers();
- headers.add(HTTP_BASIC_AUTH_HEADER, "Basic " + BASE64_USERPASS);
- assertThat(HttpBasicExtractor.INSTANCE.apply(headers)).isEqualTo(Pair.of(USERNAME, PASSWORD));
- }
-
- @Test
- public void extractUsernamePasswordCustomHeaders() throws Exception {
- final String customHeaderUsername = "X-OpenIDM-Username";
- final String customHeaderPassword = "X-OpenIDM-Password";
- CustomHeaderExtractor cha = new CustomHeaderExtractor(customHeaderUsername, customHeaderPassword);
- Headers headers = new Headers();
- headers.add(customHeaderUsername, USERNAME);
- headers.add(customHeaderPassword, PASSWORD);
-
- assertThat(cha.apply(headers)).isEqualTo(Pair.of(USERNAME, PASSWORD));
- }
-
}
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/OptionalFilterTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/OptionalFilterTest.java
deleted file mode 100644
index bfac595..0000000
--- a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/OptionalFilterTest.java
+++ /dev/null
@@ -1,65 +0,0 @@
-/*
- * The contents of this file are subject to the terms of the Common Development and
- * Distribution License (the License). You may not use this file except in compliance with the
- * License.
- *
- * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
- * specific language governing permission and limitations under the License.
- *
- * When distributing Covered Software, include this CDDL Header Notice in each file and include
- * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
- * Header, with the fields enclosed by brackets [] replaced by your own identifying
- * information: "Portions copyright [year] [name of copyright owner]".
- *
- * Copyright 2016 ForgeRock AS.
- */
-package org.forgerock.opendj.rest2ldap.authz;
-
-import static org.mockito.Matchers.*;
-import static org.mockito.Mockito.*;
-
-import org.forgerock.http.Filter;
-import org.forgerock.http.Handler;
-import org.forgerock.http.protocol.Request;
-import org.forgerock.opendj.rest2ldap.authz.OptionalFilter.Condition;
-import org.forgerock.services.context.Context;
-import org.forgerock.services.context.RootContext;
-import org.forgerock.testng.ForgeRockTestCase;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Test;
-
-@Test
-public class OptionalFilterTest extends ForgeRockTestCase {
-
- private OptionalFilter optionalFilter;
- private Filter filter;
- private Condition condition;
-
- @BeforeMethod
- public void setUp() {
- filter = mock(Filter.class);
- condition = mock(Condition.class);
- optionalFilter = new OptionalFilter(filter, condition);
- }
-
- @Test
- public void testFilterNotAppliedIfConditionIsFalse() {
- when(condition.canApplyFilter(any(Context.class), any(Request.class))).thenReturn(false);
-
- optionalFilter.filter(new RootContext(), new Request(), mock(Handler.class));
-
- verify(filter, never()).filter(any(RootContext.class), any(Request.class), any(Handler.class));
- }
-
- @Test
- public void testFilterAppliedIfConditionIsTrue() {
- when(condition.canApplyFilter(any(Context.class), any(Request.class))).thenReturn(true);
-
- final Context context = new RootContext();
- final Request request = new Request();
- final Handler handler = mock(Handler.class);
- optionalFilter.filter(context, request, handler);
-
- verify(filter).filter(same(context), same(request), same(handler));
- }
-}
diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2FilterTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2FilterTest.java
index b61e35b..d0747a2 100644
--- a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2FilterTest.java
+++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/authz/ProxiedAuthV2FilterTest.java
@@ -95,7 +95,7 @@
.isEqualTo(ByteString.valueOfUtf8("dn:uid=whatever,ou=people,dc=com"));
}
}));
- filter = new ProxiedAuthV2Filter(connectionFactory, ProxiedAuthV2Filter.IntrospectionAuthzProvider.INSTANCE);
+ filter = new ProxiedAuthV2Filter(connectionFactory);
final Map<String, Object> authz = new HashMap<>();
authz.put(SecurityContext.AUTHZID_DN, "uid=whatever,ou=people,dc=com");
diff --git a/opendj-server-legacy/resource/config/http-config.json b/opendj-server-legacy/resource/config/http-config.json
index da8f01b..3c0311b 100644
--- a/opendj-server-legacy/resource/config/http-config.json
+++ b/opendj-server-legacy/resource/config/http-config.json
@@ -3,16 +3,8 @@
// The authorization policies to use. Supported policies are "anonymous", "basic" and "oauth2".
"policies": [ "basic" ],
- // Perform all operations using a pre-authorization connection.
- "anonymous": {
- // Specify the connection factory to use to perform LDAP operations.
- // If missing, "root" factory will be used.
- "ldapConnectionFactory": "root",
-
- // Enable proxied authorization using the specified user DN.
- // If empty, anonymous proxied authorization will be used.
- // If missing, connection from the ldapConnectionFactory will be used as-is.
- "userDN": ""
+ "anonymous": {
+ // Perform all operations using anonymous user
},
// Use HTTP Basic authentication's information to bind to the LDAP server.
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/InternalProxyAuthzFilter.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/InternalProxyAuthzFilter.java
index 39a75c2..21452d9 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/InternalProxyAuthzFilter.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/InternalProxyAuthzFilter.java
@@ -16,9 +16,13 @@
package org.opends.server.protocols.http.rest2ldap;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
+import static org.forgerock.services.context.SecurityContext.AUTHZID_DN;
+import static org.forgerock.services.context.SecurityContext.AUTHZID_ID;
import static org.forgerock.util.Reject.checkNotNull;
import static org.forgerock.util.Utils.closeSilently;
+import java.util.Map;
+
import org.forgerock.http.Filter;
import org.forgerock.http.Handler;
import org.forgerock.http.protocol.Request;
@@ -34,7 +38,6 @@
import org.forgerock.opendj.rest2ldap.AuthenticatedConnectionContext;
import org.forgerock.services.context.Context;
import org.forgerock.services.context.SecurityContext;
-import org.forgerock.util.Function;
import org.forgerock.util.promise.NeverThrowsException;
import org.forgerock.util.promise.Promise;
import org.forgerock.util.promise.Promises;
@@ -53,14 +56,11 @@
{
private final IdentityMapper<?> identityMapper;
private final Schema schema;
- private final Function<SecurityContext, String, LdapException> authzIdProvider;
- InternalProxyAuthzFilter(IdentityMapper<?> identityMapper, Schema schema,
- Function<SecurityContext, String, LdapException> authzIdProvider)
+ InternalProxyAuthzFilter(IdentityMapper<?> identityMapper, Schema schema)
{
this.identityMapper = checkNotNull(identityMapper, "identityMapper cannot be null");
this.schema = checkNotNull(schema, "schema cannot be null");
- this.authzIdProvider = checkNotNull(authzIdProvider, "authzIdProvider cannot be null");
}
@Override
@@ -92,27 +92,27 @@
private DN getUserDN(final SecurityContext securityContext) throws LdapException, DirectoryException
{
- final String authzId = authzIdProvider.apply(securityContext);
- if (authzId.startsWith("u:"))
- {
- final Entry entry = identityMapper.getEntryForID(authzId);
- if (entry == null)
- {
- throw LdapException.newLdapException(ResultCode.INVALID_CREDENTIALS);
- }
- return entry.getName();
- }
- else if (authzId.startsWith("dn:"))
+ final Map<String, Object> authz = securityContext.getAuthorization();
+ if (authz.containsKey(AUTHZID_DN))
{
try
{
- return DN.valueOf(authzId.substring(3), schema);
+ return DN.valueOf(authz.get(AUTHZID_DN).toString(), schema);
}
catch (LocalizedIllegalArgumentException e)
{
throw LdapException.newLdapException(ResultCode.INVALID_DN_SYNTAX, e);
}
}
+ if (authz.containsKey(AUTHZID_ID))
+ {
+ final Entry entry = identityMapper.getEntryForID(authz.get(AUTHZID_ID).toString());
+ if (entry == null)
+ {
+ throw LdapException.newLdapException(ResultCode.INVALID_CREDENTIALS);
+ }
+ return entry.getName();
+ }
throw LdapException.newLdapException(ResultCode.AUTHORIZATION_DENIED);
}
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEndpoint.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEndpoint.java
index 706347e..f87f501 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEndpoint.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/rest2ldap/Rest2LdapEndpoint.java
@@ -15,8 +15,9 @@
*/
package org.opends.server.protocols.http.rest2ldap;
-import static org.opends.messages.ConfigMessages.*;
-import static org.opends.server.util.StaticUtils.*;
+import static org.opends.messages.ConfigMessages.ERR_CONFIG_REST2LDAP_MALFORMED_URL;
+import static org.opends.server.util.StaticUtils.getFileForPath;
+import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString;
import java.net.MalformedURLException;
import java.net.URI;
@@ -27,15 +28,15 @@
import org.forgerock.http.HttpApplication;
import org.forgerock.opendj.adapter.server3x.Adapters;
import org.forgerock.opendj.ldap.ConnectionFactory;
-import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.opendj.rest2ldap.Rest2LDAPHttpApplication;
+import org.forgerock.opendj.rest2ldap.authz.ConditionalFilters.ConditionalFilter;
import org.forgerock.opendj.server.config.server.Rest2ldapEndpointCfg;
-import org.forgerock.services.context.SecurityContext;
-import org.forgerock.util.Function;
import org.opends.server.api.HttpEndpoint;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.ServerContext;
+import org.opends.server.protocols.internal.InternalClientConnection;
+import org.opends.server.types.AuthenticationInfo;
import org.opends.server.types.InitializationException;
/**
@@ -82,22 +83,28 @@
private final class InternalRest2LDAPHttpApplication extends Rest2LDAPHttpApplication
{
private final ConnectionFactory rootInternalConnectionFactory = Adapters.newRootConnectionFactory();
+ private final ConnectionFactory anonymousInternalConnectionFactory =
+ Adapters.newConnectionFactory(new InternalClientConnection((AuthenticationInfo) null));
- InternalRest2LDAPHttpApplication(URL configURL, Schema schema)
+ InternalRest2LDAPHttpApplication(final URL configURL, final Schema schema)
{
super(configURL, schema);
}
@Override
- protected Filter newProxyAuthzFilter(final ConnectionFactory connectionFactory,
- final Function<SecurityContext, String, LdapException> authzIdProvider)
+ protected ConditionalFilter newAnonymousFilter(final ConnectionFactory connectionFactory)
{
- return new InternalProxyAuthzFilter(DirectoryServer.getProxiedAuthorizationIdentityMapper(), schema,
- authzIdProvider);
+ return super.newAnonymousFilter(anonymousInternalConnectionFactory);
}
@Override
- protected ConnectionFactory getConnectionFactory(String name)
+ protected Filter newProxyAuthzFilter(final ConnectionFactory connectionFactory)
+ {
+ return new InternalProxyAuthzFilter(DirectoryServer.getProxiedAuthorizationIdentityMapper(), schema);
+ }
+
+ @Override
+ protected ConnectionFactory getConnectionFactory(final String name)
{
return rootInternalConnectionFactory;
}
--
Gitblit v1.10.0