Partial fix for OPENDJ-694: Implement HTTP BASIC authentication
* add support for using proxied authorization control
* add support for re-using connections obtained during authentication (e.g. in authentication servlet filter)
* refactored JSON config to allow configuration of multiple connection factories.
3 files added
6 files modified
| | |
| | | */ |
| | | public final class Rest2LDAPConnectionFactoryProvider { |
| | | private static final String INIT_PARAM_CONFIG_FILE = "config-file"; |
| | | |
| | | private static final ObjectMapper JSON_MAPPER = new ObjectMapper(); |
| | | |
| | | /** |
| | |
| | | * |
| | | * <pre> |
| | | * { |
| | | * // The LDAP server configuration - see Rest2LDAP.configureConnectionFactory(JsonValue). |
| | | * "primaryLDAPServers" : [ |
| | | * { |
| | | * "hostname" : "host1.example.com", |
| | | * "port" : 389 |
| | | * } |
| | | * ], |
| | | * |
| | | * // LDAP connection factory configurations. |
| | | * "ldapConnectionFactories" : { |
| | | * "default" : { |
| | | * // See Rest2LDAP.configureConnectionFactory(JsonValue, String) |
| | | * }, |
| | | * "root" : { |
| | | * ... |
| | | * } |
| | | * }, |
| | | * |
| | | * // This is optional. |
| | | * "authorization" : { |
| | | * // The LDAP connection factory which should be used for LDAP operations, or |
| | | * // re-use cached connection from authentication filter if not present. |
| | | * "ldapConnectionFactory" : "root", |
| | | * |
| | | * // The optional authorization ID template to use if proxied authorization is |
| | | * // to be performed. |
| | | * "proxyAuthzIdTemplate" : "dn:uid={uid},ou=people,dc=example,dc=com" |
| | | * }, |
| | | * |
| | | * // The LDAP mappings |
| | | * "mappings" : { |
| | |
| | | * @return The configured JSON resource connection factory. |
| | | * @throws ServletException |
| | | * If the connection factory could not be initialized. |
| | | * @see Rest2LDAP#configureConnectionFactory(JsonValue) |
| | | * @see Rest2LDAP#configureConnectionFactory(JsonValue, String) |
| | | * @see Builder#configureMapping(JsonValue) |
| | | */ |
| | | public static ConnectionFactory getConnectionFactory(final ServletConfig config) |
| | |
| | | } |
| | | final JsonValue configuration = new JsonValue(content); |
| | | |
| | | // Parse the LDAP connection configuration. |
| | | final org.forgerock.opendj.ldap.ConnectionFactory ldapFactory = |
| | | Rest2LDAP.configureConnectionFactory(configuration); |
| | | // Parse the authorization configuration. |
| | | final String proxyAuthzTemplate = |
| | | configuration.get("authorization").get("proxyAuthzIdTemplate").asString(); |
| | | final String ldapFactoryName = |
| | | configuration.get("authorization").get("ldapConnectionFactory").asString(); |
| | | final org.forgerock.opendj.ldap.ConnectionFactory ldapFactory; |
| | | if (ldapFactoryName != null) { |
| | | ldapFactory = |
| | | Rest2LDAP.configureConnectionFactory(configuration.get( |
| | | "ldapConnectionFactories").required(), ldapFactoryName); |
| | | } else { |
| | | ldapFactory = null; |
| | | } |
| | | |
| | | // Create the router. |
| | | final Router router = new Router(); |
| | |
| | | for (final String mappingUrl : mappings.keys()) { |
| | | final JsonValue mapping = mappings.get(mappingUrl); |
| | | final CollectionResourceProvider provider = |
| | | Rest2LDAP.builder().connectionFactory(ldapFactory) |
| | | .configureMapping(mapping).build(); |
| | | Rest2LDAP.builder().connectionFactory(ldapFactory).useProxiedAuthorization( |
| | | proxyAuthzTemplate).configureMapping(mapping).build(); |
| | | router.addRoute(mappingUrl, provider); |
| | | } |
| | | return Resources.newInternalConnectionFactory(router); |
| | |
| | | { |
| | | "ldapConnectionFactories" : { |
| | | "default" : { |
| | | "primaryLDAPServers" : [ |
| | | { |
| | | "hostname" : "localhost", |
| | | "port" : 1389 |
| | | } |
| | | ], |
| | | |
| | | "connectionPoolSize" : 10, |
| | | "heartBeatIntervalSeconds" : 30 |
| | | }, |
| | | "root" : { |
| | | "inheritFrom" : "default", |
| | | "authentication" : { |
| | | "simple" : { |
| | | "bindDN" : "cn=directory manager", |
| | | "password" : "password" |
| | | "bindPassword" : "password" |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | |
| | | "connectionPoolSize" : 10, |
| | | "heartBeatIntervalSeconds" : 30, |
| | | "authorization" : { |
| | | "ldapConnectionFactory" : "root" |
| | | }, |
| | | |
| | | "mappings" : { |
| | | "/users" : { |
| 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 2013 ForgeRock AS. |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static org.forgerock.opendj.rest2ldap.Utils.ensureNotNull; |
| | | |
| | | import org.forgerock.json.fluent.JsonValue; |
| | | import org.forgerock.json.resource.Context; |
| | | import org.forgerock.json.resource.InternalServerErrorException; |
| | | import org.forgerock.json.resource.PersistenceConfig; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | |
| | | /** |
| | | * A {@link Context} containing a cached pre-authenticated LDAP connection which |
| | | * should be re-used for performing subsequent LDAP operations. The LDAP |
| | | * connection is typically acquired while perform authentication in an HTTP |
| | | * servlet filter. It is the responsibility of the component which acquired the |
| | | * connection to release once processing has completed. |
| | | */ |
| | | public final class AuthenticatedConnectionContext extends Context { |
| | | /* |
| | | * TODO: this context does not support persistence because there is no |
| | | * obvious way to restore the connection. We could just persist the context |
| | | * and restore it as null, and let rest2ldap switch to using the factory + |
| | | * proxied authz. |
| | | */ |
| | | private final Connection connection; |
| | | |
| | | /** |
| | | * Creates a new pre-authenticated cached LDAP connection context having the |
| | | * provided parent and an ID automatically generated using |
| | | * {@code UUID.randomUUID()}. |
| | | * |
| | | * @param parent |
| | | * The parent context. |
| | | * @param connection |
| | | * The cached pre-authenticated LDAP connection which should be |
| | | * re-used for subsequent LDAP operations. |
| | | */ |
| | | public AuthenticatedConnectionContext(final Context parent, final Connection connection) { |
| | | super(ensureNotNull(parent)); |
| | | this.connection = connection; |
| | | } |
| | | |
| | | /** |
| | | * Creates a new pre-authenticated cached LDAP connection context having the |
| | | * provided ID and parent. |
| | | * |
| | | * @param id |
| | | * The context ID. |
| | | * @param parent |
| | | * The parent context. |
| | | * @param connection |
| | | * The cached pre-authenticated LDAP connection which should be |
| | | * re-used for subsequent LDAP operations. |
| | | */ |
| | | public AuthenticatedConnectionContext(final String id, final Context parent, |
| | | final Connection connection) { |
| | | super(id, ensureNotNull(parent)); |
| | | this.connection = connection; |
| | | } |
| | | |
| | | /** |
| | | * Restore from JSON representation. |
| | | * |
| | | * @param savedContext |
| | | * The JSON representation from which this context's attributes |
| | | * should be parsed. |
| | | * @param config |
| | | * The persistence configuration. |
| | | * @throws ResourceException |
| | | * If the JSON representation could not be parsed. |
| | | */ |
| | | AuthenticatedConnectionContext(final JsonValue savedContext, final PersistenceConfig config) |
| | | throws ResourceException { |
| | | super(savedContext, config); |
| | | throw new InternalServerErrorException("Cached LDAP connections cannot be restored"); |
| | | } |
| | | |
| | | /** |
| | | * {@inheritDoc} |
| | | */ |
| | | @Override |
| | | protected void saveToJson(final JsonValue savedContext, final PersistenceConfig config) |
| | | throws ResourceException { |
| | | super.saveToJson(savedContext, config); |
| | | throw new InternalServerErrorException("Cached LDAP connections cannot be persisted"); |
| | | } |
| | | |
| | | /** |
| | | * Returns the cached pre-authenticated LDAP connection which should be |
| | | * re-used for subsequent LDAP operations. |
| | | * |
| | | * @return The cached pre-authenticated LDAP connection which should be |
| | | * re-used for subsequent LDAP operations. |
| | | */ |
| | | Connection getConnection() { |
| | | return connection; |
| | | } |
| | | } |
| 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 2013 ForgeRock AS. |
| | | */ |
| | | |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static org.forgerock.opendj.rest2ldap.Utils.isJSONPrimitive; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.Locale; |
| | | import java.util.Map; |
| | | import java.util.regex.Matcher; |
| | | import java.util.regex.Pattern; |
| | | |
| | | import org.forgerock.json.resource.ForbiddenException; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.schema.Schema; |
| | | |
| | | /** |
| | | * An authorization ID template used for mapping security context principals to |
| | | * AuthzID templates of the form |
| | | * <code>dn:uid={uid},ou={realm},dc=example,dc=com</code>, or |
| | | * <code>u:{uid}@{realm}.example.com</code>. |
| | | */ |
| | | final class AuthzIdTemplate { |
| | | private static final Pattern KEY_RE = Pattern.compile("\\{([^}]+)\\}"); |
| | | |
| | | private final String dnFormatString; |
| | | private final String formatString; |
| | | private final List<String> keys = new ArrayList<String>(); |
| | | private final String template; |
| | | |
| | | AuthzIdTemplate(final String template) { |
| | | if (!template.startsWith("u:") && !template.startsWith("dn:")) { |
| | | throw new IllegalArgumentException("Invalid authorization ID template: " + template); |
| | | } |
| | | |
| | | // Parse the template keys and replace them with %s for formatting. |
| | | final Matcher matcher = KEY_RE.matcher(template); |
| | | final StringBuffer buffer = new StringBuffer(template.length()); |
| | | while (matcher.find()) { |
| | | matcher.appendReplacement(buffer, "%s"); |
| | | keys.add(matcher.group(1)); |
| | | } |
| | | matcher.appendTail(buffer); |
| | | |
| | | this.template = template; |
| | | this.formatString = buffer.toString(); |
| | | this.dnFormatString = template.startsWith("dn:") ? formatString.substring(3) : null; |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | return template; |
| | | } |
| | | |
| | | String formatAsAuthzId(final Map<String, Object> principals, final Schema schema) |
| | | throws ResourceException { |
| | | if (isDNTemplate()) { |
| | | final String dn = formatAsDN(principals, schema).toString(); |
| | | final StringBuilder builder = new StringBuilder(dn.length() + 3); |
| | | builder.append("dn:"); |
| | | builder.append(dn); |
| | | return builder.toString(); |
| | | } else { |
| | | final String[] values = getPrincipalsForFormatting(principals); |
| | | return String.format(Locale.ENGLISH, formatString, (Object[]) values); |
| | | } |
| | | } |
| | | |
| | | DN formatAsDN(final Map<String, Object> principals, final Schema schema) |
| | | throws ResourceException { |
| | | if (!isDNTemplate()) { |
| | | throw new IllegalStateException(); |
| | | } |
| | | final String[] values = getPrincipalsForFormatting(principals); |
| | | return DN.format(dnFormatString, schema, (Object[]) values); |
| | | } |
| | | |
| | | boolean isDNTemplate() { |
| | | return dnFormatString != null; |
| | | } |
| | | |
| | | private String[] getPrincipalsForFormatting(final Map<String, Object> principals) |
| | | throws ForbiddenException { |
| | | final String[] values = new String[keys.size()]; |
| | | for (int i = 0; i < values.length; i++) { |
| | | final String key = keys.get(i); |
| | | final Object value = principals.get(key); |
| | | if (isJSONPrimitive(value)) { |
| | | values[i] = String.valueOf(value); |
| | | } else if (value == null) { |
| | | // FIXME: i18n. |
| | | throw new ForbiddenException( |
| | | "The request could not be authorized because the required security principal " |
| | | + key + " could not be determined"); |
| | | } else { |
| | | // FIXME: i18n. |
| | | throw new ForbiddenException( |
| | | "The request could not be authorized because the required security principal " |
| | | + key + " had an invalid data type"); |
| | | } |
| | | } |
| | | return values; |
| | | } |
| | | } |
| | |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import org.forgerock.opendj.ldap.ConnectionFactory; |
| | | import org.forgerock.opendj.ldap.DecodeOptions; |
| | | import org.forgerock.opendj.ldap.schema.Schema; |
| | | |
| | |
| | | * Common configuration options. |
| | | */ |
| | | final class Config { |
| | | private final ConnectionFactory factory; |
| | | private final DecodeOptions options; |
| | | private final AuthzIdTemplate proxiedAuthzTemplate; |
| | | private final ReadOnUpdatePolicy readOnUpdatePolicy; |
| | | private final Schema schema; |
| | | |
| | | Config(final ReadOnUpdatePolicy readOnUpdatePolicy, final Schema schema) { |
| | | Config(final ConnectionFactory factory, final ReadOnUpdatePolicy readOnUpdatePolicy, |
| | | final AuthzIdTemplate proxiedAuthzTemplate, final Schema schema) { |
| | | this.factory = factory; |
| | | this.readOnUpdatePolicy = readOnUpdatePolicy; |
| | | this.proxiedAuthzTemplate = proxiedAuthzTemplate; |
| | | this.schema = schema; |
| | | this.options = new DecodeOptions().setSchema(schema); |
| | | } |
| | | |
| | | /** |
| | | * Returns the LDAP SDK connection factory which should be used when |
| | | * performing LDAP operations. |
| | | * |
| | | * @return The LDAP SDK connection factory which should be used when |
| | | * performing LDAP operations. |
| | | */ |
| | | ConnectionFactory connectionFactory() { |
| | | return factory; |
| | | } |
| | | |
| | | /** |
| | | * Returns the decoding options which should be used when decoding controls |
| | | * in responses. |
| | | * |
| | | * @return The decoding options which should be used when decoding controls |
| | | * in responses. |
| | | */ |
| | | public DecodeOptions decodeOptions() { |
| | | DecodeOptions decodeOptions() { |
| | | return options; |
| | | } |
| | | |
| | | /** |
| | | * Returns the authorization ID template which should be used when proxied |
| | | * authorization is enabled. |
| | | * |
| | | * @return The authorization ID template which should be used when proxied |
| | | * authorization is enabled, or {@code null} if proxied |
| | | * authorization is disabled. |
| | | */ |
| | | AuthzIdTemplate getProxiedAuthorizationTemplate() { |
| | | return proxiedAuthzTemplate; |
| | | } |
| | | |
| | | /** |
| | | * Returns the policy which should be used in order to read an entry before |
| | | * it is deleted, or after it is added or modified. |
| | | * |
| | | * @return The policy which should be used in order to read an entry before |
| | | * it is deleted, or after it is added or modified. |
| | | */ |
| | | public ReadOnUpdatePolicy readOnUpdatePolicy() { |
| | | ReadOnUpdatePolicy readOnUpdatePolicy() { |
| | | return readOnUpdatePolicy; |
| | | } |
| | | |
| | |
| | | * @return The schema which should be used when attribute types and |
| | | * controls. |
| | | */ |
| | | public Schema schema() { |
| | | Schema schema() { |
| | | return schema; |
| | | } |
| | | |
| | | /** |
| | | * Returns {@code true} if the proxied authorization should be used for |
| | | * authorizing LDAP requests. |
| | | * |
| | | * @return {@code true} if the proxied authorization should be used for |
| | | * authorizing LDAP requests. |
| | | */ |
| | | boolean useProxiedAuthorization() { |
| | | return proxiedAuthzTemplate != null; |
| | | } |
| | | } |
| | |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static org.forgerock.opendj.ldap.ErrorResultException.newErrorResult; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.adapt; |
| | | |
| | | import java.io.Closeable; |
| | | import java.util.LinkedHashMap; |
| | |
| | | import java.util.concurrent.CountDownLatch; |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | |
| | | import org.forgerock.json.resource.InternalServerErrorException; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.json.resource.SecurityContext; |
| | | import org.forgerock.json.resource.ServerContext; |
| | | import org.forgerock.opendj.ldap.AbstractAsynchronousConnection; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | |
| | | import org.forgerock.opendj.ldap.ResultHandler; |
| | | import org.forgerock.opendj.ldap.SearchResultHandler; |
| | | import org.forgerock.opendj.ldap.SearchScope; |
| | | import org.forgerock.opendj.ldap.controls.Control; |
| | | import org.forgerock.opendj.ldap.controls.ProxiedAuthV2RequestControl; |
| | | import org.forgerock.opendj.ldap.requests.AbandonRequest; |
| | | import org.forgerock.opendj.ldap.requests.AddRequest; |
| | | import org.forgerock.opendj.ldap.requests.BindRequest; |
| | |
| | | import org.forgerock.opendj.ldap.requests.ExtendedRequest; |
| | | import org.forgerock.opendj.ldap.requests.ModifyDNRequest; |
| | | import org.forgerock.opendj.ldap.requests.ModifyRequest; |
| | | import org.forgerock.opendj.ldap.requests.Request; |
| | | import org.forgerock.opendj.ldap.requests.SearchRequest; |
| | | import org.forgerock.opendj.ldap.requests.UnbindRequest; |
| | | import org.forgerock.opendj.ldap.responses.BindResult; |
| | |
| | | }; |
| | | |
| | | private final Config config; |
| | | |
| | | private final AtomicReference<Connection> connection = new AtomicReference<Connection>(); |
| | | |
| | | private final ServerContext context; |
| | | private final Connection preAuthenticatedConnection; |
| | | private Control proxiedAuthzControl = null; |
| | | |
| | | Context(final Config config, final ServerContext context) { |
| | | this.config = config; |
| | | this.context = context; |
| | | if (context.containsContext(AuthenticatedConnectionContext.class)) { |
| | | final Connection connection = |
| | | context.asContext(AuthenticatedConnectionContext.class).getConnection(); |
| | | this.preAuthenticatedConnection = connection != null ? wrap(connection) : null; |
| | | } else { |
| | | this.preAuthenticatedConnection = null; |
| | | } |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | @Override |
| | | public void close() { |
| | | /* |
| | | * Only release the connection that we acquired. Don't release the |
| | | * cached connection since that is the responsibility of the component |
| | | * which acquired it. |
| | | */ |
| | | final Connection c = connection.getAndSet(null); |
| | | if (c != null) { |
| | | c.close(); |
| | |
| | | } |
| | | |
| | | Connection getConnection() { |
| | | return connection.get(); |
| | | return preAuthenticatedConnection != null ? preAuthenticatedConnection : connection.get(); |
| | | } |
| | | |
| | | ServerContext getServerContext() { |
| | | return context; |
| | | } |
| | | |
| | | void setConnection(final Connection connection) { |
| | | if (!this.connection.compareAndSet(null, withCache(connection))) { |
| | | throw new IllegalStateException("LDAP connection obtained multiple times"); |
| | | /** |
| | | * Performs common processing required before handling an HTTP request, |
| | | * including calculating the proxied authorization request control, and |
| | | * obtaining an LDAP connection. |
| | | * <p> |
| | | * This method should be called at most once per request. |
| | | * |
| | | * @param handler |
| | | * The result handler which should be invoked if an error is |
| | | * detected. |
| | | * @param runnable |
| | | * The runnable which will be invoked once the common processing |
| | | * has completed. Implementations will be able to call |
| | | * {@link #getConnection()} to get the LDAP connection for use |
| | | * with subsequent LDAP requests. |
| | | */ |
| | | void run(final org.forgerock.json.resource.ResultHandler<?> handler, final Runnable runnable) { |
| | | /* |
| | | * Compute the proxied authorization control from the content of the |
| | | * security context if present. Only do this if we are not using a |
| | | * cached connection since cached connections are supposed to have been |
| | | * pre-authenticated and therefore do not require proxied authorization. |
| | | */ |
| | | if (preAuthenticatedConnection == null && config.useProxiedAuthorization()) { |
| | | if (context.containsContext(SecurityContext.class)) { |
| | | try { |
| | | final SecurityContext securityContext = |
| | | context.asContext(SecurityContext.class); |
| | | final String authzId = |
| | | config.getProxiedAuthorizationTemplate().formatAsAuthzId( |
| | | securityContext.getAuthorizationId(), config.schema()); |
| | | proxiedAuthzControl = ProxiedAuthV2RequestControl.newControl(authzId); |
| | | } catch (final ResourceException e) { |
| | | handler.handleError(e); |
| | | return; |
| | | } |
| | | } else { |
| | | // FIXME: i18n. |
| | | handler.handleError(new InternalServerErrorException( |
| | | "The request could not be authorized because it did not contain a security context")); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | /* |
| | | * Adds read caching support to the provided connection. |
| | | * Now get the LDAP connection to use for processing subsequent LDAP |
| | | * requests. A null factory indicates that Rest2LDAP has been configured |
| | | * to re-use the LDAP connection which was used for authentication. |
| | | */ |
| | | private Connection withCache(final Connection connection) { |
| | | if (preAuthenticatedConnection != null) { |
| | | // Invoke the handler immediately since a connection is available. |
| | | runnable.run(); |
| | | } else if (config.connectionFactory() != null) { |
| | | config.connectionFactory().getConnectionAsync(new ResultHandler<Connection>() { |
| | | @Override |
| | | public final void handleErrorResult(final ErrorResultException error) { |
| | | handler.handleError(adapt(error)); |
| | | } |
| | | |
| | | @Override |
| | | public final void handleResult(final Connection result) { |
| | | if (!connection.compareAndSet(null, wrap(result))) { |
| | | // This should never happen. |
| | | throw new IllegalStateException("LDAP connection obtained multiple times"); |
| | | } |
| | | runnable.run(); |
| | | } |
| | | }); |
| | | } else { |
| | | // FIXME: i18n |
| | | handler.handleError(new InternalServerErrorException( |
| | | "The request could not be processed because there was no LDAP connection available for use")); |
| | | } |
| | | } |
| | | |
| | | /* |
| | | * Adds read caching support to the provided connection as well |
| | | * functionality which automatically adds the proxied authorization control |
| | | * if needed. |
| | | */ |
| | | private Connection wrap(final Connection connection) { |
| | | /* |
| | | * We only use async methods so no need to wrap sync methods. |
| | | */ |
| | | return new AbstractAsynchronousConnection() { |
| | | |
| | | @Override |
| | | public FutureResult<Void> abandonAsync(final AbandonRequest request) { |
| | | return connection.abandonAsync(request); |
| | |
| | | public FutureResult<Result> addAsync(final AddRequest request, |
| | | final IntermediateResponseHandler intermediateResponseHandler, |
| | | final ResultHandler<? super Result> resultHandler) { |
| | | return connection.addAsync(request, intermediateResponseHandler, resultHandler); |
| | | return connection.addAsync(withControls(request), intermediateResponseHandler, |
| | | resultHandler); |
| | | } |
| | | |
| | | @Override |
| | |
| | | public FutureResult<CompareResult> compareAsync(final CompareRequest request, |
| | | final IntermediateResponseHandler intermediateResponseHandler, |
| | | final ResultHandler<? super CompareResult> resultHandler) { |
| | | return connection.compareAsync(request, intermediateResponseHandler, resultHandler); |
| | | return connection.compareAsync(withControls(request), intermediateResponseHandler, |
| | | resultHandler); |
| | | } |
| | | |
| | | @Override |
| | |
| | | final IntermediateResponseHandler intermediateResponseHandler, |
| | | final ResultHandler<? super Result> resultHandler) { |
| | | evict(request.getName()); |
| | | return connection.deleteAsync(request, intermediateResponseHandler, resultHandler); |
| | | return connection.deleteAsync(withControls(request), intermediateResponseHandler, |
| | | resultHandler); |
| | | } |
| | | |
| | | @Override |
| | |
| | | * operation modifies an entry: clear the cachedReads. |
| | | */ |
| | | evictAll(); |
| | | return connection.extendedRequestAsync(request, intermediateResponseHandler, |
| | | resultHandler); |
| | | return connection.extendedRequestAsync(withControls(request), |
| | | intermediateResponseHandler, resultHandler); |
| | | } |
| | | |
| | | @Override |
| | |
| | | final IntermediateResponseHandler intermediateResponseHandler, |
| | | final ResultHandler<? super Result> resultHandler) { |
| | | evict(request.getName()); |
| | | return connection.modifyAsync(request, intermediateResponseHandler, resultHandler); |
| | | return connection.modifyAsync(withControls(request), intermediateResponseHandler, |
| | | resultHandler); |
| | | } |
| | | |
| | | @Override |
| | |
| | | final ResultHandler<? super Result> resultHandler) { |
| | | // Simple brute force implementation: clear the cachedReads. |
| | | evictAll(); |
| | | return connection |
| | | .modifyDNAsync(request, intermediateResponseHandler, resultHandler); |
| | | return connection.modifyDNAsync(withControls(request), intermediateResponseHandler, |
| | | resultHandler); |
| | | } |
| | | |
| | | @Override |
| | |
| | | public FutureResult<Result> searchAsync(final SearchRequest request, |
| | | final IntermediateResponseHandler intermediateResponseHandler, |
| | | final SearchResultHandler resultHandler) { |
| | | |
| | | /* |
| | | * Don't attempt caching if this search is not a read (base |
| | | * object), or if the search request passed in an intermediate |
| | |
| | | */ |
| | | if (!request.getScope().equals(SearchScope.BASE_OBJECT) |
| | | || intermediateResponseHandler != null) { |
| | | return connection.searchAsync(request, intermediateResponseHandler, |
| | | resultHandler); |
| | | return connection.searchAsync(withControls(request), |
| | | intermediateResponseHandler, resultHandler); |
| | | } |
| | | |
| | | // This is a read request and a candidate for caching. |
| | |
| | | cachedReads.put(request.getName(), pendingCachedRead); |
| | | } |
| | | final FutureResult<Result> future = |
| | | connection.searchAsync(request, intermediateResponseHandler, |
| | | pendingCachedRead); |
| | | connection.searchAsync(withControls(request), |
| | | intermediateResponseHandler, pendingCachedRead); |
| | | pendingCachedRead.setFuture(future); |
| | | return future; |
| | | } |
| | |
| | | cachedReads.clear(); |
| | | } |
| | | } |
| | | |
| | | private <R extends Request> R withControls(final R request) { |
| | | if (proxiedAuthzControl != null) { |
| | | request.addControl(proxiedAuthzControl); |
| | | } |
| | | return request; |
| | | } |
| | | }; |
| | | } |
| | | |
| | |
| | | import org.forgerock.json.resource.UncategorizedException; |
| | | import org.forgerock.json.resource.UpdateRequest; |
| | | import org.forgerock.opendj.ldap.Attribute; |
| | | import org.forgerock.opendj.ldap.Connection; |
| | | import org.forgerock.opendj.ldap.ConnectionFactory; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.DecodeException; |
| | | import org.forgerock.opendj.ldap.Entry; |
| | |
| | | * resource collection to LDAP entries beneath a base DN. |
| | | */ |
| | | final class LDAPCollectionResourceProvider implements CollectionResourceProvider { |
| | | private abstract class ConnectionCompletionHandler implements |
| | | org.forgerock.opendj.ldap.ResultHandler<Connection> { |
| | | private final Context c; |
| | | private final ResultHandler<?> handler; |
| | | |
| | | ConnectionCompletionHandler(final Context c, final ResultHandler<?> handler) { |
| | | this.c = c; |
| | | this.handler = handler; |
| | | } |
| | | |
| | | @Override |
| | | public final void handleErrorResult(final ErrorResultException error) { |
| | | handler.handleError(adapt(error)); |
| | | } |
| | | |
| | | @Override |
| | | public final void handleResult(final Connection connection) { |
| | | c.setConnection(connection); |
| | | chain(); |
| | | } |
| | | |
| | | abstract void chain(); |
| | | } |
| | | |
| | | // Dummy exception used for signalling search success. |
| | | private static final ResourceException SUCCESS = new UncategorizedException(0, null, null); |
| | | |
| | | private final List<Attribute> additionalLDAPAttributes; |
| | | |
| | | private final AttributeMapper attributeMapper; |
| | | |
| | | private final DN baseDN; // TODO: support template variables. |
| | | private final Config config; |
| | | private final ConnectionFactory factory; |
| | | private final MVCCStrategy mvccStrategy; |
| | | private final NameStrategy nameStrategy; |
| | | |
| | | LDAPCollectionResourceProvider(final DN baseDN, final AttributeMapper mapper, |
| | | final ConnectionFactory factory, final NameStrategy nameStrategy, |
| | | final MVCCStrategy mvccStrategy, final Config config, |
| | | final NameStrategy nameStrategy, final MVCCStrategy mvccStrategy, final Config config, |
| | | final List<Attribute> additionalLDAPAttributes) { |
| | | this.baseDN = baseDN; |
| | | this.attributeMapper = mapper; |
| | | this.factory = factory; |
| | | this.config = config; |
| | | this.nameStrategy = nameStrategy; |
| | | this.mvccStrategy = mvccStrategy; |
| | |
| | | final ResultHandler<Resource> h = wrap(c, handler); |
| | | |
| | | // Get the connection, then determine entry content, then perform add. |
| | | factory.getConnectionAsync(new ConnectionCompletionHandler(c, h) { |
| | | c.run(h, new Runnable() { |
| | | @Override |
| | | void chain() { |
| | | public void run() { |
| | | // Calculate entry content. |
| | | attributeMapper.toLDAP(c, request.getContent(), |
| | | new ResultHandler<List<Attribute>>() { |
| | |
| | | final QueryResultHandler h = wrap(c, handler); |
| | | |
| | | // Get the connection, then calculate the search filter, then perform the search. |
| | | factory.getConnectionAsync(new ConnectionCompletionHandler(c, h) { |
| | | c.run(h, new Runnable() { |
| | | @Override |
| | | void chain() { |
| | | public void run() { |
| | | // Calculate the filter (this may require the connection). |
| | | getLDAPFilter(c, request.getQueryFilter(), new ResultHandler<Filter>() { |
| | | @Override |
| | |
| | | final ResultHandler<Resource> h = wrap(c, handler); |
| | | |
| | | // Get connection then perform the search. |
| | | factory.getConnectionAsync(new ConnectionCompletionHandler(c, h) { |
| | | c.run(h, new Runnable() { |
| | | @Override |
| | | void chain() { |
| | | public void run() { |
| | | // Do the search. |
| | | final String[] attributes = getLDAPAttributes(c, request.getFieldFilters()); |
| | | final SearchRequest request = |
| | |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.Arrays; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.LinkedList; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.Set; |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | |
| | | private ConnectionFactory factory; |
| | | private MVCCStrategy mvccStrategy; |
| | | private NameStrategy nameStrategy; |
| | | private AuthzIdTemplate proxiedAuthzTemplate; |
| | | private ReadOnUpdatePolicy readOnUpdatePolicy = CONTROLS; |
| | | private AttributeMapper rootMapper; |
| | | private Schema schema = Schema.getDefaultSchema(); |
| | |
| | | } |
| | | |
| | | public CollectionResourceProvider build() { |
| | | ensureNotNull(factory); |
| | | ensureNotNull(baseDN); |
| | | if (rootMapper == null) { |
| | | throw new IllegalStateException("No mappings provided"); |
| | | } |
| | | return new LDAPCollectionResourceProvider(baseDN, rootMapper, factory, nameStrategy, |
| | | mvccStrategy, new Config(readOnUpdatePolicy, schema), additionalLDAPAttributes); |
| | | if (proxiedAuthzTemplate != null && factory == null) { |
| | | throw new IllegalStateException( |
| | | "No connection factory specified for use with proxied authorization"); |
| | | } |
| | | |
| | | /** |
| | | * Configures the connection factory using the provided JSON |
| | | * configuration. See |
| | | * {@link Rest2LDAP#configureConnectionFactory(JsonValue)} for a |
| | | * detailed specification of the JSON configuration. |
| | | * |
| | | * @param configuration |
| | | * The JSON configuration. |
| | | * @return A reference to this builder. |
| | | * @throws IllegalArgumentException |
| | | * If the configuration is invalid. |
| | | */ |
| | | public Builder configureConnectionFactory(final JsonValue configuration) { |
| | | connectionFactory(Rest2LDAP.configureConnectionFactory(configuration)); |
| | | return this; |
| | | return new LDAPCollectionResourceProvider(baseDN, rootMapper, nameStrategy, |
| | | mvccStrategy, new Config(factory, readOnUpdatePolicy, proxiedAuthzTemplate, |
| | | schema), additionalLDAPAttributes); |
| | | } |
| | | |
| | | /** |
| | |
| | | } |
| | | |
| | | public Builder connectionFactory(final ConnectionFactory factory) { |
| | | ensureNotNull(factory); |
| | | this.factory = factory; |
| | | return this; |
| | | } |
| | |
| | | return useEtagAttribute(ad(attribute)); |
| | | } |
| | | |
| | | public Builder useProxiedAuthorization(final String template) { |
| | | this.proxiedAuthzTemplate = template != null ? new AuthzIdTemplate(template) : null; |
| | | return this; |
| | | } |
| | | |
| | | public Builder useServerEntryUUIDNaming(final AttributeType dnAttribute) { |
| | | return useServerNaming(dnAttribute, AttributeDescription |
| | | .create(getEntryUUIDAttributeType())); |
| | |
| | | } |
| | | |
| | | /** |
| | | * Creates a new connection factory using the provided JSON configuration. |
| | | * The configuration should look like this, excluding the C-like comments: |
| | | * Creates a new connection factory using the named configuration in the |
| | | * provided JSON list of factory configurations. Excluding the C-like |
| | | * comments, the configuration should look like this: |
| | | * |
| | | * <pre> |
| | | * { |
| | | * // A default pool of servers using no authentication. |
| | | * "default" : { |
| | | * // The primary data center, must contain at least one LDAP server. |
| | | * "primaryLDAPServers" : [ |
| | | * { |
| | |
| | | * |
| | | * // Connection pool configuration. |
| | | * "connectionPoolSize" : 10, |
| | | * "heartBeatIntervalSeconds" : 30, |
| | | * |
| | | * // SSL/TLS configuration (optional and TBD). |
| | | * "useSSL" : { |
| | | * // Elect to use StartTLS instead of SSL. |
| | | * "useStartTLS" : true, |
| | | * ... |
| | | * "heartBeatIntervalSeconds" : 30 |
| | | * }, |
| | | * |
| | | * // Authentication configuration (optional and TBD). |
| | | * // The same pool of servers except authenticated as cn=directory manager. |
| | | * "root" : { |
| | | * "inheritFrom" : "default", |
| | | * "authentication" : { |
| | | * "simple" : { |
| | | * "bindDN" : "cn=directory manager", |
| | | * "password" : "password" |
| | | * }, |
| | | * "bindPassword" : "password" |
| | | * } |
| | | * } |
| | | * } |
| | | * } |
| | | * </pre> |
| | | * |
| | | * @param configuration |
| | | * The JSON configuration. |
| | | * @param name |
| | | * The name of the connection factory configuration to be parsed. |
| | | * @return A new connection factory using the provided JSON configuration. |
| | | * @throws IllegalArgumentException |
| | | * If the configuration is invalid. |
| | | */ |
| | | public static ConnectionFactory configureConnectionFactory(final JsonValue configuration) { |
| | | public static ConnectionFactory configureConnectionFactory(final JsonValue configuration, |
| | | final String name) { |
| | | final JsonValue normalizedConfiguration = |
| | | normalizeConnectionFactory(configuration, name, 0); |
| | | return configureConnectionFactory(normalizedConfiguration); |
| | | } |
| | | |
| | | public static AttributeMapper constant(final Object value) { |
| | | return new JSONConstantAttributeMapper(value); |
| | | } |
| | | |
| | | public static ObjectAttributeMapper object() { |
| | | return new ObjectAttributeMapper(); |
| | | } |
| | | |
| | | public static ReferenceAttributeMapper reference(final AttributeDescription attribute, |
| | | final DN baseDN, final AttributeDescription primaryKey, final AttributeMapper mapper) { |
| | | return new ReferenceAttributeMapper(attribute, baseDN, primaryKey, mapper); |
| | | } |
| | | |
| | | public static ReferenceAttributeMapper reference(final String attribute, final String baseDN, |
| | | final String primaryKey, final AttributeMapper mapper) { |
| | | return reference(AttributeDescription.valueOf(attribute), DN.valueOf(baseDN), |
| | | AttributeDescription.valueOf(primaryKey), mapper); |
| | | } |
| | | |
| | | public static SimpleAttributeMapper simple(final AttributeDescription attribute) { |
| | | return new SimpleAttributeMapper(attribute); |
| | | } |
| | | |
| | | public static SimpleAttributeMapper simple(final String attribute) { |
| | | return simple(AttributeDescription.valueOf(attribute)); |
| | | } |
| | | |
| | | private static ConnectionFactory configureConnectionFactory(final JsonValue configuration) { |
| | | // Parse pool parameters, |
| | | final int connectionPoolSize = |
| | | Math.max(configuration.get("connectionPoolSize").defaultTo(10).asInteger(), 1); |
| | |
| | | final BindRequest bindRequest; |
| | | if (configuration.isDefined("authentication")) { |
| | | final JsonValue authn = configuration.get("authentication"); |
| | | if (authn.isDefined("simple")) { |
| | | final JsonValue simple = authn.get("simple"); |
| | | bindRequest = |
| | | Requests.newSimpleBindRequest(authn.get("bindDN").required().asString(), authn |
| | | .get("password").required().asString().toCharArray()); |
| | | Requests.newSimpleBindRequest(simple.get("bindDN").required().asString(), |
| | | simple.get("bindPassword").required().asString().toCharArray()); |
| | | } else { |
| | | throw new IllegalArgumentException("Only simple authentication is supported"); |
| | | } |
| | | } else { |
| | | bindRequest = null; |
| | | } |
| | |
| | | } |
| | | } |
| | | |
| | | public static AttributeMapper constant(final Object value) { |
| | | return new JSONConstantAttributeMapper(value); |
| | | private static JsonValue normalizeConnectionFactory(final JsonValue configuration, |
| | | final String name, final int depth) { |
| | | // Protect against infinite recursion in the configuration. |
| | | if (depth > 100) { |
| | | throw new IllegalArgumentException( |
| | | "The LDAP server configuration '" |
| | | + name |
| | | + "' could not be parsed because of potential circular inheritance dependencies"); |
| | | } |
| | | |
| | | public static ObjectAttributeMapper object() { |
| | | return new ObjectAttributeMapper(); |
| | | final JsonValue current = configuration.get(name).required(); |
| | | if (current.isDefined("inheritFrom")) { |
| | | // Inherit missing fields from inherited configuration. |
| | | final JsonValue parent = |
| | | normalizeConnectionFactory(configuration, |
| | | current.get("inheritFrom").asString(), depth + 1); |
| | | final Map<String, Object> normalized = |
| | | new LinkedHashMap<String, Object>(parent.asMap()); |
| | | normalized.putAll(current.asMap()); |
| | | normalized.remove("inheritFrom"); |
| | | return new JsonValue(normalized); |
| | | } else { |
| | | // No normalization required. |
| | | return current; |
| | | } |
| | | |
| | | public static ReferenceAttributeMapper reference(final AttributeDescription attribute, |
| | | final DN baseDN, final AttributeDescription primaryKey, final AttributeMapper mapper) { |
| | | return new ReferenceAttributeMapper(attribute, baseDN, primaryKey, mapper); |
| | | } |
| | | |
| | | public static ReferenceAttributeMapper reference(final String attribute, final String baseDN, |
| | | final String primaryKey, final AttributeMapper mapper) { |
| | | return reference(AttributeDescription.valueOf(attribute), DN.valueOf(baseDN), |
| | | AttributeDescription.valueOf(primaryKey), mapper); |
| | | } |
| | | |
| | | public static SimpleAttributeMapper simple(final AttributeDescription attribute) { |
| | | return new SimpleAttributeMapper(attribute); |
| | | } |
| | | |
| | | public static SimpleAttributeMapper simple(final String attribute) { |
| | | return simple(AttributeDescription.valueOf(attribute)); |
| | | } |
| | | |
| | | private static ConnectionFactory parseLDAPServers(final JsonValue config, |
| 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 2013 ForgeRock AS. |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static org.fest.assertions.Assertions.assertThat; |
| | | |
| | | import java.util.LinkedHashMap; |
| | | import java.util.Map; |
| | | |
| | | import org.forgerock.json.resource.ForbiddenException; |
| | | import org.forgerock.opendj.ldap.schema.Schema; |
| | | import org.forgerock.testng.ForgeRockTestCase; |
| | | import org.testng.annotations.DataProvider; |
| | | import org.testng.annotations.Test; |
| | | |
| | | /** |
| | | * Tests the AuthzIdTemplate class. |
| | | */ |
| | | @SuppressWarnings({ "javadoc" }) |
| | | @Test |
| | | public final class AuthzIdTemplateTest extends ForgeRockTestCase { |
| | | |
| | | @DataProvider |
| | | public Object[][] templateData() { |
| | | // @formatter:off |
| | | return new Object[][] { |
| | | { |
| | | "dn:uid={uid},ou={realm},dc=example,dc=com", |
| | | "dn:uid=test.user,ou=acme,dc=example,dc=com", |
| | | map("uid", "test.user", "realm", "acme") |
| | | }, |
| | | { |
| | | // Should perform DN quoting. |
| | | "dn:uid={uid},ou={realm},dc=example,dc=com", |
| | | "dn:uid=test.user,ou=test\\+cn\\=quoting,dc=example,dc=com", |
| | | map("uid", "test.user", "realm", "test+cn=quoting") |
| | | }, |
| | | { |
| | | "u:{uid}@{realm}.example.com", |
| | | "u:test.user@acme.example.com", |
| | | map("uid", "test.user", "realm", "acme") |
| | | }, |
| | | { |
| | | // Should not perform any DN quoting. |
| | | "u:{uid}@{realm}.example.com", |
| | | "u:test.user@test+cn=quoting.example.com", |
| | | map("uid", "test.user", "realm", "test+cn=quoting") |
| | | }, |
| | | }; |
| | | // @formatter:on |
| | | |
| | | } |
| | | |
| | | @Test(dataProvider = "templateData") |
| | | public void testTemplates(final String template, final String expected, |
| | | Map<String, Object> principals) throws Exception { |
| | | assertThat( |
| | | new AuthzIdTemplate(template) |
| | | .formatAsAuthzId(principals, Schema.getDefaultSchema())) |
| | | .isEqualTo(expected); |
| | | } |
| | | |
| | | @DataProvider |
| | | public Object[][] invalidTemplateData() { |
| | | // @formatter:off |
| | | return new Object[][] { |
| | | { |
| | | "dn:uid={uid},ou={realm},dc=example,dc=com", |
| | | map("uid", "test.user") |
| | | }, |
| | | { |
| | | "u:{uid}@{realm}.example.com", |
| | | map("uid", "test.user") |
| | | }, |
| | | }; |
| | | // @formatter:on |
| | | |
| | | } |
| | | |
| | | @Test(dataProvider = "invalidTemplateData", expectedExceptions = ForbiddenException.class) |
| | | public void testInvalidTemplateData(final String template, Map<String, Object> principals) |
| | | throws Exception { |
| | | new AuthzIdTemplate(template).formatAsAuthzId(principals, Schema.getDefaultSchema()); |
| | | } |
| | | |
| | | @DataProvider |
| | | public Object[][] invalidTemplates() { |
| | | // @formatter:off |
| | | return new Object[][] { |
| | | { |
| | | "x:uid={uid},ou={realm},dc=example,dc=com" |
| | | }, |
| | | }; |
| | | // @formatter:on |
| | | |
| | | } |
| | | |
| | | @Test(dataProvider = "invalidTemplates", expectedExceptions = IllegalArgumentException.class) |
| | | public void testInvalidTemplates(final String template) throws Exception { |
| | | new AuthzIdTemplate(template); |
| | | } |
| | | |
| | | private Map<String, Object> map(String... keyValues) { |
| | | Map<String, Object> map = new LinkedHashMap<String, Object>(); |
| | | for (int i = 0; i < keyValues.length; i += 2) { |
| | | map.put(keyValues[i], keyValues[i + 1]); |
| | | } |
| | | return map; |
| | | } |
| | | } |