From e5e0de3b39b2b91f479fdcd09d907c69bcdaa851 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Thu, 21 Mar 2013 08:42:44 +0000
Subject: [PATCH] Partial fix for OPENDJ-694: Implement HTTP BASIC authentication

---
 opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplate.java                                    |  120 ++++++++
 opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Context.java                                            |  143 ++++++++-
 opendj-sdk/opendj3/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json                                                |   33 +
 opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java                                          |  211 ++++++++-----
 opendj-sdk/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplateTest.java                                |  123 ++++++++
 opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java                     |   45 --
 opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java                                             |   48 ++
 opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java                     |  114 +++++++
 opendj-sdk/opendj3/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPConnectionFactoryProvider.java |   48 ++
 9 files changed, 709 insertions(+), 176 deletions(-)

diff --git a/opendj-sdk/opendj3/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPConnectionFactoryProvider.java b/opendj-sdk/opendj3/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPConnectionFactoryProvider.java
index 48c3419..ba36044 100644
--- a/opendj-sdk/opendj3/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPConnectionFactoryProvider.java
+++ b/opendj-sdk/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);
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json b/opendj-sdk/opendj3/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json
index 54ad6af..cae4009 100644
--- a/opendj-sdk/opendj3/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json
+++ b/opendj-sdk/opendj3/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json
@@ -1,18 +1,29 @@
 {
-    "primaryLDAPServers" : [
-        {
-            "hostname" : "localhost",
-            "port"     : 1389
+    "ldapConnectionFactories" : {
+        "default" : {
+            "primaryLDAPServers" : [
+                {
+                    "hostname" : "localhost",
+                    "port"     : 1389
+                }
+            ],
+            "connectionPoolSize"       : 10,
+            "heartBeatIntervalSeconds" : 30
+        },
+        "root" : {
+            "inheritFrom"    : "default",
+            "authentication" : {
+                "simple" : {
+                    "bindDN"       : "cn=directory manager",
+                    "bindPassword" : "password"
+                }
+            }
         }
-    ],
-
-    "authentication" : {
-        "bindDN"   : "cn=directory manager",
-        "password" : "password"
     },
 
-    "connectionPoolSize"       : 10,
-    "heartBeatIntervalSeconds" : 30,
+    "authorization" : {
+        "ldapConnectionFactory" : "root"
+    },
 
     "mappings" : {
         "/users" : {
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java
new file mode 100644
index 0000000..78b8d96
--- /dev/null
+++ b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthenticatedConnectionContext.java
@@ -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;
+    }
+}
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplate.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplate.java
new file mode 100644
index 0000000..07fccaf
--- /dev/null
+++ b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplate.java
@@ -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;
+    }
+}
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java
index ca0e939..f8f2c54 100644
--- a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java
+++ b/opendj-sdk/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;
+    }
 }
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Context.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Context.java
index 0a65a76..4136956 100644
--- a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Context.java
+++ b/opendj-sdk/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;
+            }
+        }
+
+        /*
+         * 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.
+         */
+        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.
+     * Adds read caching support to the provided connection as well
+     * functionality which automatically adds the proxied authorization control
+     * if needed.
      */
-    private Connection withCache(final Connection connection) {
+    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;
+            }
         };
     }
 
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
index d2aa3fb..b4eda51 100644
--- a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
+++ b/opendj-sdk/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 =
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
index ef84e2f..11f43c4 100644
--- a/opendj-sdk/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
+++ b/opendj-sdk/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);
-        }
-
-        /**
-         * 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;
+            if (proxiedAuthzTemplate != null && factory == null) {
+                throw new IllegalStateException(
+                        "No connection factory specified for use with proxied authorization");
+            }
+            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,61 +533,99 @@
     }
 
     /**
-     * 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>
      * {
-     *     // The primary data center, must contain at least one LDAP server.
-     *     "primaryLDAPServers" : [
-     *         {
-     *             "hostname" : "host1.example.com",
-     *             "port"     : 389
-     *         },
-     *         {
-     *             "hostname" : "host2.example.com",
-     *             "port"     : 389
-     *         },
-     *     ],
+     *     // A default pool of servers using no authentication.
+     *     "default" : {
+     *         // The primary data center, must contain at least one LDAP server.
+     *         "primaryLDAPServers" : [
+     *             {
+     *                 "hostname" : "host1.example.com",
+     *                 "port"     : 389
+     *             },
+     *             {
+     *                 "hostname" : "host2.example.com",
+     *                 "port"     : 389
+     *             },
+     *         ],
      *
-     *     // The optional secondary (fail-over) data center.
-     *     "secondaryLDAPServers" : [
-     *         {
-     *             "hostname" : "host3.example.com",
-     *             "port"     : 389
-     *         },
-     *         {
-     *             "hostname" : "host4.example.com",
-     *             "port"     : 389
-     *         },
-     *     ],
+     *         // The optional secondary (fail-over) data center.
+     *         "secondaryLDAPServers" : [
+     *             {
+     *                 "hostname" : "host3.example.com",
+     *                 "port"     : 389
+     *             },
+     *             {
+     *                 "hostname" : "host4.example.com",
+     *                 "port"     : 389
+     *             },
+     *         ],
      *
-     *     // Connection pool configuration.
-     *     "connectionPoolSize"       : 10,
-     *     "heartBeatIntervalSeconds" : 30,
-     *
-     *     // SSL/TLS configuration (optional and TBD).
-     *     "useSSL" : {
-     *         // Elect to use StartTLS instead of SSL.
-     *         "useStartTLS" : true,
-     *         ...
+     *         // Connection pool configuration.
+     *         "connectionPoolSize"       : 10,
+     *         "heartBeatIntervalSeconds" : 30
      *     },
      *
-     *     // Authentication configuration (optional and TBD).
-     *     "authentication" : {
-     *         "bindDN"   : "cn=directory manager",
-     *         "password" : "password"
-     *     },
+     *     // The same pool of servers except authenticated as cn=directory manager.
+     *     "root" : {
+     *         "inheritFrom"    : "default",
+     *         "authentication" : {
+     *             "simple" : {
+     *                 "bindDN"       : "cn=directory manager",
+     *                 "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");
-            bindRequest =
-                    Requests.newSimpleBindRequest(authn.get("bindDN").required().asString(), authn
-                            .get("password").required().asString().toCharArray());
+            if (authn.isDefined("simple")) {
+                final JsonValue simple = authn.get("simple");
+                bindRequest =
+                        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();
-    }
-
-    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));
+        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;
+        }
     }
 
     private static ConnectionFactory parseLDAPServers(final JsonValue config,
diff --git a/opendj-sdk/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplateTest.java b/opendj-sdk/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplateTest.java
new file mode 100644
index 0000000..7376fce
--- /dev/null
+++ b/opendj-sdk/opendj3/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/AuthzIdTemplateTest.java
@@ -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;
+    }
+}

--
Gitblit v1.10.0