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

Matthew Swift
21.42.2013 bd58756e64a0330feb547c2ffd7de716471d98e8
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
813 ■■■■ changed files
opendj3/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPConnectionFactoryProvider.java 50 ●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json 19 ●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java 114 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplate.java 120 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java 48 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Context.java 143 ●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java 45 ●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java 151 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplateTest.java 123 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPConnectionFactoryProvider.java
@@ -36,7 +36,6 @@
 */
public final class Rest2LDAPConnectionFactoryProvider {
    private static final String INIT_PARAM_CONFIG_FILE = "config-file";
    private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
    /**
@@ -47,15 +46,26 @@
     *
     * <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" : {
@@ -80,7 +90,7 @@
     * @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)
@@ -105,9 +115,19 @@
            }
            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();
@@ -115,8 +135,8 @@
            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);
opendj3/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json
@@ -1,18 +1,29 @@
{
    "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" : {
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java
New file
@@ -0,0 +1,114 @@
/*
 * 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;
    }
}
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplate.java
New file
@@ -0,0 +1,120 @@
/*
 * 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;
    }
}
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java
@@ -15,6 +15,7 @@
 */
package org.forgerock.opendj.rest2ldap;
import org.forgerock.opendj.ldap.ConnectionFactory;
import org.forgerock.opendj.ldap.DecodeOptions;
import org.forgerock.opendj.ldap.schema.Schema;
@@ -22,35 +23,63 @@
 * 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;
    }
@@ -61,7 +90,18 @@
     * @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;
    }
}
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Context.java
@@ -16,6 +16,7 @@
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;
@@ -24,6 +25,9 @@
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;
@@ -35,6 +39,8 @@
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;
@@ -43,6 +49,7 @@
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;
@@ -190,14 +197,21 @@
    };
    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;
        }
    }
    /**
@@ -205,6 +219,11 @@
     */
    @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();
@@ -216,28 +235,98 @@
    }
    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);
@@ -247,7 +336,8 @@
            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
@@ -276,7 +366,8 @@
            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
@@ -284,7 +375,8 @@
                    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
@@ -297,8 +389,8 @@
                 * operation modifies an entry: clear the cachedReads.
                 */
                evictAll();
                return connection.extendedRequestAsync(request, intermediateResponseHandler,
                        resultHandler);
                return connection.extendedRequestAsync(withControls(request),
                        intermediateResponseHandler, resultHandler);
            }
            @Override
@@ -316,7 +408,8 @@
                    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
@@ -325,8 +418,8 @@
                    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
@@ -341,7 +434,6 @@
            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
@@ -349,8 +441,8 @@
                 */
                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.
@@ -369,8 +461,8 @@
                        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;
                }
@@ -392,6 +484,13 @@
                    cachedReads.clear();
                }
            }
            private <R extends Request> R withControls(final R request) {
                if (proxiedAuthzControl != null) {
                    request.addControl(proxiedAuthzControl);
                }
                return request;
            }
        };
    }
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
@@ -54,8 +54,6 @@
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;
@@ -79,50 +77,21 @@
 * 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;
@@ -148,9 +117,9 @@
        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>>() {
@@ -209,9 +178,9 @@
        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
@@ -331,9 +300,9 @@
        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 =
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
@@ -23,8 +23,10 @@
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;
@@ -69,6 +71,7 @@
        private ConnectionFactory factory;
        private MVCCStrategy mvccStrategy;
        private NameStrategy nameStrategy;
        private AuthzIdTemplate proxiedAuthzTemplate;
        private ReadOnUpdatePolicy readOnUpdatePolicy = CONTROLS;
        private AttributeMapper rootMapper;
        private Schema schema = Schema.getDefaultSchema();
@@ -98,30 +101,17 @@
        }
        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);
        }
        /**
@@ -243,7 +233,6 @@
        }
        public Builder connectionFactory(final ConnectionFactory factory) {
            ensureNotNull(factory);
            this.factory = factory;
            return this;
        }
@@ -313,6 +302,11 @@
            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()));
@@ -539,11 +533,14 @@
    }
    /**
     * 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" : [
     *         {
@@ -570,30 +567,65 @@
     *
     *     // 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);
@@ -604,9 +636,14 @@
        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;
        }
@@ -642,31 +679,31 @@
        }
    }
    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,
opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplateTest.java
New file
@@ -0,0 +1,123 @@
/*
 * 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;
    }
}