Rest2Ldap: Removed connection reuse, simplify authorization filtering,
use factory methods, add more unit tests.
2 files deleted
5 files added
14 files modified
| | |
| | | "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. |
| | |
| | | "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", |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | |
| | | 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. |
| | |
| | | } |
| | | |
| | | 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 |
| | |
| | | |
| | | 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( |
| | | 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 + "'"); |
| | |
| | | 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) { |
| | |
| | | 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); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 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); |
| | | } |
| | | } |
| | |
| | | */ |
| | | 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; |
| | |
| | | * 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); |
| | | } |
| New file |
| | |
| | | /* |
| | | * 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); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 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; |
| | | } |
| | | }; |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 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; |
| | | } |
| | | } |
| | | } |
| | |
| | | /** |
| | | * Inject {@link Connection} into a {@link AuthenticatedConnectionContext}. |
| | | */ |
| | | public final class DirectConnectionFilter implements Filter { |
| | | final class DirectConnectionFilter implements Filter { |
| | | |
| | | private final ConnectionFactory connectionFactory; |
| | | |
| | |
| | | |
| | | 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; |
| | | |
| | |
| | | * 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. |
| | |
| | | * 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 |
| | |
| | | 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>() { |
| | |
| | | ? 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; |
| | | } |
| | | }); |
| | | } |
| | | } |
| | |
| | | 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; |
| | |
| | | * 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 |
| | |
| | | * @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 |
| | |
| | | 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; |
| | | } |
| | |
| | | .thenFinally(close(connectionHolder)); |
| | | } |
| | | |
| | | 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 { |
| | | private String resolveAuthorizationId(SecurityContext securityContext) throws LdapException { |
| | | Object candidate; |
| | | candidate = securityContext.getAuthorization().get(AUTHZID_DN); |
| | | if (candidate != null) { |
| | |
| | | } |
| | | 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 Connection newProxiedConnection(Connection baseConnection, String authzId) { |
| | | return new CachedReadConnectionDecorator( |
| | | new ProxiedAuthConnectionDecorator(baseConnection, newControl(authzId))); |
| | | } |
| | | |
| | | private static final class ProxiedAuthConnectionDecorator extends AbstractAsynchronousConnectionDecorator { |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | |
| | | @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))) |
| | |
| | | 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; |
| | | |
| | |
| | | |
| | | @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() |
| | |
| | | @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)); |
| | | } |
| | | }); |
| | | } |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | |
| | | @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( |
| New file |
| | |
| | | /* |
| | | * 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(); |
| | | } |
| | | } |
| | |
| | | */ |
| | | 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; |
| | |
| | | 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); |
| | |
| | | .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)); |
| | | } |
| | | |
| | | } |
| | |
| | | .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"); |
| | |
| | | // 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": "" |
| | | // Perform all operations using anonymous user |
| | | }, |
| | | |
| | | // Use HTTP Basic authentication's information to bind to the LDAP server. |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | { |
| | | 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 |
| | |
| | | |
| | | 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); |
| | | } |
| | | |
| | |
| | | */ |
| | | 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; |
| | |
| | | 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; |
| | | |
| | | /** |
| | |
| | | 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; |
| | | } |