From 9020a676bbe359cb158e96761ef6f1a3c32c80e5 Mon Sep 17 00:00:00 2001
From: Yannick Lecaillez <yannick.lecaillez@forgerock.com>
Date: Tue, 10 May 2016 16:42:27 +0000
Subject: [PATCH] REST2LDAP Refactoring
---
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java | 402 +++++++++++++++++++++++++++++++++++++++++++++-----------
1 files changed, 321 insertions(+), 81 deletions(-)
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 54a8b25..41a3b08 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
@@ -11,123 +11,134 @@
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
- * Copyright 2015 ForgeRock AS.
+ * Copyright 2015-2016 ForgeRock AS.
*/
package org.forgerock.opendj.rest2ldap;
-import static org.forgerock.http.util.Json.*;
+import static org.forgerock.http.util.Json.readJsonLenient;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.configureConnectionFactory;
-import static org.forgerock.util.Utils.*;
+import static org.forgerock.util.Reject.checkNotNull;
+import static org.forgerock.util.Utils.closeSilently;
-import java.io.Closeable;
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 org.forgerock.http.Filter;
import org.forgerock.http.Handler;
import org.forgerock.http.HttpApplication;
import org.forgerock.http.HttpApplicationException;
+import org.forgerock.http.filter.Filters;
import org.forgerock.http.handler.Handlers;
import org.forgerock.http.io.Buffer;
+import org.forgerock.http.protocol.Headers;
import org.forgerock.http.protocol.Request;
import org.forgerock.http.protocol.Response;
import org.forgerock.json.JsonValue;
-import org.forgerock.json.resource.CollectionResourceProvider;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.Router;
import org.forgerock.json.resource.http.CrestHttp;
+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.services.context.Context;
+import org.forgerock.services.context.SecurityContext;
import org.forgerock.util.Factory;
-import org.forgerock.util.Reject;
+import org.forgerock.util.Function;
+import org.forgerock.util.Pair;
import org.forgerock.util.promise.NeverThrowsException;
import org.forgerock.util.promise.Promise;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Rest2ldap HTTP application. */
-public final class Rest2LDAPHttpApplication implements HttpApplication {
+public class Rest2LDAPHttpApplication implements HttpApplication {
+ private static final String DEFAULT_ROOT_FACTORY = "root";
+ private static final String DEFAULT_BIND_FACTORY = "bind";
+
private static final Logger LOG = LoggerFactory.getLogger(Rest2LDAPHttpApplication.class);
- private static final class HttpHandler implements Handler, Closeable {
- private final ConnectionFactory ldapConnectionFactory;
- private final Handler delegate;
+ /** URL to the JSON configuration file. */
+ protected final URL configurationUrl;
- HttpHandler(final JsonValue configuration) {
- ldapConnectionFactory = createLdapConnectionFactory(configuration);
- try {
- delegate = CrestHttp.newHttpHandler(createRouter(configuration, ldapConnectionFactory));
- } catch (final RuntimeException e) {
- closeSilently(ldapConnectionFactory);
- throw e;
- }
- }
+ /** Schema used to perform DN validations. */
+ protected final Schema schema;
- private static RequestHandler createRouter(
- final JsonValue configuration, final ConnectionFactory ldapConnectionFactory) {
- final AuthorizationPolicy authzPolicy = configuration.get("servlet")
- .get("authorizationPolicy")
- .required()
- .asEnum(AuthorizationPolicy.class);
- final String proxyAuthzTemplate = configuration.get("servlet").get("proxyAuthzIdTemplate").asString();
- final JsonValue mappings = configuration.get("servlet").get("mappings").required();
+ private final Map<String, ConnectionFactory> connectionFactories = new HashMap<>();
- final Router router = new Router();
- for (final String mappingUrl : mappings.keys()) {
- final JsonValue mapping = mappings.get(mappingUrl);
- final CollectionResourceProvider provider = Rest2LDAP.builder()
- .ldapConnectionFactory(ldapConnectionFactory)
- .authorizationPolicy(authzPolicy)
- .proxyAuthzIdTemplate(proxyAuthzTemplate)
- .configureMapping(mapping)
- .build();
- router.addRoute(Router.uriTemplate(mappingUrl), provider);
- }
- return router;
- }
+ private enum Policy {
+ oauth2 (0),
+ basic (50),
+ anonymous (100);
- private static ConnectionFactory createLdapConnectionFactory(final JsonValue configuration) {
- final String ldapFactoryName = configuration.get("servlet").get("ldapConnectionFactory").asString();
- if (ldapFactoryName != null) {
- return configureConnectionFactory(
- configuration.get("ldapConnectionFactories").required(), ldapFactoryName);
- }
- return null;
- }
+ private final int priority;
- @Override
- public void close() {
- closeSilently(ldapConnectionFactory);
- }
-
- @Override
- public Promise<Response, NeverThrowsException> handle(final Context context, final Request request) {
- return delegate.handle(context, request);
+ Policy(int priority) {
+ this.priority = priority;
}
}
- private final URL configurationUrl;
- private HttpHandler handler;
- private HttpAuthenticationFilter filter;
+ private enum BindStrategy {
+ simple, search, sasl_plain
+ }
/**
- * Default constructor called by the HTTP Framework which will use the
- * default configuration file location.
+ * Default constructor called by the HTTP Framework which will use the default configuration file location.
*/
public Rest2LDAPHttpApplication() {
this.configurationUrl = getClass().getResource("/opendj-rest2ldap-config.json");
+ this.schema = Schema.getDefaultSchema();
}
/**
* Creates a new Rest2LDAP HTTP application using the provided configuration URL.
*
* @param configurationURL
- * The URL to the JSON configuration file.
+ * The URL to the JSON configuration file
+ * @param schema
+ * The {@link Schema} used to perform DN validations
*/
- public Rest2LDAPHttpApplication(final URL configurationURL) {
- Reject.ifNull(configurationURL, "The configuration URL must not be null");
- this.configurationUrl = configurationURL;
+ public Rest2LDAPHttpApplication(final URL configurationURL, final Schema schema) {
+ this.configurationUrl = checkNotNull(configurationURL, "configurationURL cannot be null");
+ this.schema = checkNotNull(schema, "schema cannot be null");
+ }
+
+ @Override
+ public final Handler start() throws HttpApplicationException {
+ try {
+ final JsonValue configuration = readJson(configurationUrl);
+ configureConnectionFactories(configuration.get("ldapConnectionFactories"));
+ return Handlers.chainOf(
+ CrestHttp.newHttpHandler(configureRest2Ldap(configuration)),
+ newAuthorizationFilter(configuration.get("authorization").required()));
+ } catch (final Exception e) {
+ // TODO i18n, once supported in opendj-rest2ldap
+ final String errorMsg = "Unable to start Rest2Ldap Http Application";
+ LOG.error(errorMsg, e);
+ stop();
+ throw new HttpApplicationException(errorMsg, e);
+ }
}
private static JsonValue readJson(final URL resource) throws IOException {
@@ -136,19 +147,20 @@
}
}
- @Override
- public Handler start() throws HttpApplicationException {
- try {
- final JsonValue configuration = readJson(configurationUrl);
- handler = new HttpHandler(configuration);
- filter = new HttpAuthenticationFilter(configuration);
- return Handlers.chainOf(handler, filter);
- } catch (final Exception e) {
- // TODO i18n, once supported in opendj-rest2ldap
- final String errorMsg = "Unable to start Rest2Ldap Http Application";
- LOG.error(errorMsg, e);
- stop();
- throw new HttpApplicationException(errorMsg, e);
+ private static RequestHandler configureRest2Ldap(final JsonValue configuration) {
+ final JsonValue mappings = configuration.get("mappings").required();
+ final Router router = new Router();
+ for (final String mappingUrl : mappings.keys()) {
+ final JsonValue mapping = mappings.get(mappingUrl);
+ router.addRoute(Router.uriTemplate(mappingUrl), Rest2LDAP.builder().configureMapping(mapping).build());
+ }
+ return router;
+ }
+
+ private void configureConnectionFactories(final JsonValue config) {
+ connectionFactories.clear();
+ for (String name : config.keys()) {
+ connectionFactories.put(name, configureConnectionFactory(config, name));
}
}
@@ -160,8 +172,236 @@
@Override
public void stop() {
- closeSilently(handler, filter);
- handler = null;
- filter = null;
+ for (ConnectionFactory factory : connectionFactories.values()) {
+ closeSilently(factory);
+ }
+ connectionFactories.clear();
+ }
+
+ 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) {
+ return new Filter() {
+ @Override
+ public Promise<Response, NeverThrowsException> filter(Context context, Request request, Handler next) {
+ return next.handle(new SecurityContext(context, authenticationId, authorization), request);
+ }
+ };
+ }
+
+ /**
+ * Create 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);
+ }
+
+ /**
+ * Get a {@link ConnectionFactory} from its name.
+ *
+ * @param name
+ * Name of the {@link ConnectionFactory} as specified in the configuration
+ * @return The associated {@link ConnectionFactory} or null if none can be found
+ */
+ protected ConnectionFactory getConnectionFactory(final String name) {
+ return connectionFactories.get(name);
+ }
+
+ private ConditionalFilter buildBasicFilter(final JsonValue config) {
+ final String bind = config.get("bind").required().asString();
+ final BindStrategy strategy = BindStrategy.valueOf(bind.toLowerCase().replace('-', '_'));
+ return newBasicAuthenticationFilter(buildBindStrategy(strategy, config.get(bind).required()),
+ config.get("supportAltAuthentication").defaultTo(Boolean.FALSE).asBoolean()
+ ? new CustomHeaderExtractor(
+ config.get("altAuthenticationUsernameHeader").required().asString(),
+ config.get("altAuthenticationPasswordHeader").required().asString())
+ : HttpBasicExtractor.INSTANCE,
+ config.get("reuseAuthenticatedConnection").defaultTo(Boolean.FALSE).asBoolean());
+ }
+
+ /**
+ * Get 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);
+ }
+
+ private AuthenticationStrategy buildBindStrategy(final BindStrategy strategy, final JsonValue config) {
+ switch (strategy) {
+ case simple:
+ return buildSimpleBindStrategy(config);
+ case search:
+ return buildSearchThenBindStrategy(config);
+ case sasl_plain:
+ return buildSASLBindStrategy(config);
+ default:
+ throw new IllegalArgumentException("Unsupported strategy '" + strategy + "'");
+ }
+ }
+
+ private AuthenticationStrategy buildSimpleBindStrategy(final JsonValue config) {
+ return newSimpleBindStrategy(getConnectionFactory(config.get("ldapConnectionFactory")
+ .defaultTo(DEFAULT_BIND_FACTORY).asString()),
+ config.get("bindDNTemplate").defaultTo("%s").asString(),
+ 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);
+ }
+
+ private AuthenticationStrategy buildSearchThenBindStrategy(JsonValue config) {
+ return newSearchThenBindStrategy(
+ getConnectionFactory(
+ config.get("searchLDAPConnectionFactory").defaultTo(DEFAULT_ROOT_FACTORY).asString()),
+ getConnectionFactory(
+ config.get("bindLDAPConnectionFactory").defaultTo(DEFAULT_BIND_FACTORY).asString()),
+ DN.valueOf(config.get("baseDN").required().asString(), schema),
+ 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);
}
}
--
Gitblit v1.10.0