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

Gaetan Boismal
18.27.2016 c8585baebc9fc35ed12a3321acf47730c967b5d3
opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAPHttpApplication.java
@@ -17,6 +17,7 @@
package org.forgerock.opendj.rest2ldap;
import static org.forgerock.http.util.Json.readJsonLenient;
import static org.forgerock.json.JsonValueFunctions.duration;
import static org.forgerock.json.JsonValueFunctions.enumConstant;
import static org.forgerock.json.JsonValueFunctions.setOf;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.configureConnectionFactory;
@@ -24,11 +25,17 @@
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.opendj.rest2ldap.authz.Authorizations.newCtsAccessTokenResolver;
import static org.forgerock.opendj.rest2ldap.authz.Authorizations.newFileAccessTokenResolver;
import static org.forgerock.opendj.rest2ldap.authz.Authorizations.newRfc7662AccessTokenResolver;
import static org.forgerock.util.Reject.checkNotNull;
import static org.forgerock.util.Utils.closeSilently;
import static org.forgerock.opendj.rest2ldap.authz.Authorizations.newConditionalOAuth2ResourceServerFilter;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
@@ -36,15 +43,25 @@
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import org.forgerock.authz.modules.oauth2.AccessTokenInfo;
import org.forgerock.authz.modules.oauth2.AccessTokenException;
import org.forgerock.authz.modules.oauth2.AccessTokenResolver;
import org.forgerock.authz.modules.oauth2.cache.CachingAccessTokenResolver;
import org.forgerock.authz.modules.oauth2.resolver.OpenAmAccessTokenResolver;
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.handler.HttpClientHandler;
import org.forgerock.http.io.Buffer;
import org.forgerock.http.protocol.Headers;
import org.forgerock.json.JsonValue;
import org.forgerock.json.JsonValueException;
import org.forgerock.json.resource.RequestHandler;
import org.forgerock.json.resource.Router;
import org.forgerock.json.resource.http.CrestHttp;
@@ -59,7 +76,12 @@
import org.forgerock.util.Factory;
import org.forgerock.util.Function;
import org.forgerock.util.Pair;
import org.forgerock.util.PerItemEvictionStrategyCache;
import org.forgerock.util.annotations.VisibleForTesting;
import org.forgerock.util.promise.NeverThrowsException;
import org.forgerock.util.promise.Promise;
import org.forgerock.util.time.Duration;
import org.forgerock.util.time.TimeService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -68,6 +90,18 @@
    private static final String DEFAULT_ROOT_FACTORY = "root";
    private static final String DEFAULT_BIND_FACTORY = "bind";
    /** Keys for json oauth2 configuration. */
    private static final String RESOLVER_CONFIG_OBJECT = "resolver";
    private static final String REALM = "realm";
    private static final String SCOPES = "requiredScopes";
    private static final String AUTHZID_TEMPLATE = "authzIdTemplate";
    private static final String CACHE_EXPIRATION_DEFAULT = "5 minutes";
    /** Keys for json oauth2 access token cache configuration. */
    private static final String CACHE_CONFIG_OBJECT = "accessTokenCache";
    private static final String CACHE_ENABLED = "enabled";
    private static final String CACHE_EXPIRATION = "cacheExpiration";
    private static final Logger LOG = LoggerFactory.getLogger(Rest2LDAPHttpApplication.class);
    /** URL to the JSON configuration file. */
@@ -77,8 +111,19 @@
    protected final Schema schema;
    private final Map<String, ConnectionFactory> connectionFactories = new HashMap<>();
    /** Used for token caching. */
    private ScheduledExecutorService executorService;
    private enum Policy { BASIC, ANONYMOUS }
    /** Define the method which should be used to resolve an OAuth2 access token. */
    private enum OAuth2ResolverType {
        RFC7662,
        OPENAM,
        CTS,
        FILE
    }
    @VisibleForTesting
    enum Policy { OAUTH2, BASIC, ANONYMOUS }
    private enum BindStrategy { SIMPLE, SEARCH, SASL_PLAIN }
@@ -107,6 +152,7 @@
    public final Handler start() throws HttpApplicationException {
        try {
            final JsonValue configuration = readJson(configurationUrl);
            executorService = Executors.newSingleThreadScheduledExecutor();
            configureConnectionFactories(configuration.get("ldapConnectionFactories"));
            return Handlers.chainOf(
                    CrestHttp.newHttpHandler(configureRest2Ldap(configuration)),
@@ -155,11 +201,18 @@
            closeSilently(factory);
        }
        connectionFactories.clear();
        if (executorService != null) {
            executorService.shutdown();
            executorService = null;
        }
    }
    private Filter buildAuthorizationFilter(final JsonValue config) {
    private Filter buildAuthorizationFilter(final JsonValue config) throws HttpApplicationException {
        final Set<Policy> policies = config.get("policies").as(setOf(enumConstant(Policy.class)));
        final List<ConditionalFilter> filters = new ArrayList<>(policies.size());
        if (policies.contains(Policy.OAUTH2)) {
            filters.add(buildOAuth2Filter(config.get("oauth2")));
        }
        if (policies.contains(Policy.BASIC)) {
            filters.add(buildBasicFilter(config.get("basic")));
        }
@@ -169,6 +222,88 @@
        return newAuthorizationFilter(filters);
    }
    @VisibleForTesting
    ConditionalFilter buildOAuth2Filter(final JsonValue config) throws HttpApplicationException {
        final String realm = config.get(REALM).defaultTo("no_realm").asString();
        final Set<String> scopes = config.get(SCOPES).required().asSet(String.class);
        final AccessTokenResolver resolver =
                createCachedTokenResolverIfNeeded(config, parseUnderlyingResolver(config));
        final ConditionalFilter oAuth2Filter = newConditionalOAuth2ResourceServerFilter(
                realm, scopes, resolver, config.get(AUTHZID_TEMPLATE).required().asString());
        return newConditionalFilter(
                Filters.chainOf(oAuth2Filter.getFilter(),
                                newProxyAuthzFilter(getConnectionFactory(DEFAULT_ROOT_FACTORY))),
                oAuth2Filter.getCondition());
    }
    @VisibleForTesting
    AccessTokenResolver createCachedTokenResolverIfNeeded(
            final JsonValue config, final AccessTokenResolver resolver) {
        final JsonValue cacheConfig = config.get(CACHE_CONFIG_OBJECT);
        if (cacheConfig.isNull() || !cacheConfig.get(CACHE_ENABLED).defaultTo(Boolean.FALSE).asBoolean()) {
            return resolver;
        }
        final Duration expiration = parseCacheExpiration(
                cacheConfig.get(CACHE_EXPIRATION).defaultTo(CACHE_EXPIRATION_DEFAULT));
        final PerItemEvictionStrategyCache<String, Promise<AccessTokenInfo, AccessTokenException>> cache =
                new PerItemEvictionStrategyCache<>(executorService, expiration);
        cache.setMaxTimeout(expiration);
        return new CachingAccessTokenResolver(TimeService.SYSTEM, resolver, cache);
    }
    @VisibleForTesting
    AccessTokenResolver parseUnderlyingResolver(final JsonValue configuration) throws HttpApplicationException {
        final JsonValue resolver = configuration.get(RESOLVER_CONFIG_OBJECT).required();
        switch (resolver.as(enumConstant(OAuth2ResolverType.class))) {
        case RFC7662:
            return parseRfc7662Resolver(configuration);
        case OPENAM:
            return new OpenAmAccessTokenResolver(new HttpClientHandler(),
                                                 TimeService.SYSTEM,
                                                 configuration.get("openam").get("endpointURL").required().asString());
        case CTS:
            final JsonValue cts = configuration.get("cts").required();
            return newCtsAccessTokenResolver(
                getConnectionFactory(cts.get("ldapConnectionFactory").defaultTo(DEFAULT_ROOT_FACTORY).asString()),
                                     cts.get("baseDN").required().asString());
        case FILE:
            return newFileAccessTokenResolver(configuration.get("file").get("folderPath").required().asString());
        default:
            throw new JsonValueException(resolver, "is not a supported access token resolver");
        }
    }
    private AccessTokenResolver parseRfc7662Resolver(final JsonValue configuration) throws HttpApplicationException {
        final JsonValue rfc7662 = configuration.get("rfc7662").required();
        final String introspectionEndPointURL = rfc7662.get("endpointURL").required().asString();
        try {
            return newRfc7662AccessTokenResolver(new HttpClientHandler(),
                                                 new URI(introspectionEndPointURL),
                                                 rfc7662.get("clientId").required().asString(),
                                                 rfc7662.get("clientSecret").required().asString());
        } catch (final URISyntaxException e) {
            throw new IllegalArgumentException("The token introspection endpoint '"
                    + introspectionEndPointURL + "' URL has an invalid syntax: " + e.getLocalizedMessage(), e);
        }
    }
    private Duration parseCacheExpiration(final JsonValue expirationJson) {
        try {
            final Duration expiration = expirationJson.as(duration());
            if (expiration.isZero() || expiration.isUnlimited()) {
                throw new JsonValueException(expirationJson, "The cache expiration duration cannot be "
                        + (expiration.isZero() ? "zero" : "unlimited."));
            }
            return expiration;
        } catch (final Exception e) {
            throw new JsonValueException(expirationJson,
                      "Malformed duration value '" + expirationJson.toString() + "' for cache expiration. "
                    + "The duration syntax supports all human readable notations from day ('days'', 'day'', 'd'') "
                    + "to nanosecond ('nanoseconds', 'nanosecond', 'nanosec', 'nanos', 'nano', 'ns')");
        }
    }
    /**
     * Creates a new {@link Filter} in charge of injecting {@link AuthenticatedConnectionContext}.
     *
@@ -263,7 +398,7 @@
    private AuthenticationStrategy buildSASLBindStrategy(JsonValue config) {
        return newSASLPlainStrategy(
                getConnectionFactory(config.get("ldapConnectionFactory").defaultTo(DEFAULT_BIND_FACTORY).asString()),
                schema, config.get("authcIdTemplate").defaultTo("u:%s").asString());
                schema, config.get(AUTHZID_TEMPLATE).defaultTo("u:%s").asString());
    }
    private AuthenticationStrategy buildSearchThenBindStrategy(JsonValue config) {