From 19536840ae2c706f4ad9ff4742e52dfc5f6ebea7 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Sat, 23 Mar 2013 01:46:37 +0000
Subject: [PATCH] Partial fix for OPENDJ-694: Implement HTTP BASIC authentication

---
 opendj3/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/web.xml                                                              |   22 +
 opendj3/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json                                                |    8 
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Context.java                                            |    4 
 opendj3/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPContextFactory.java            |    4 
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java                              |    7 
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java                                          |  205 +++++----------
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java                                              |   57 ----
 opendj3/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPAuthnFilter.java               |  393 ++++++++++++++++++++++++++++++
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java                     |    8 
 opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java                           |   12 
 opendj3/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPConnectionFactoryProvider.java |    2 
 11 files changed, 512 insertions(+), 210 deletions(-)

diff --git a/opendj3/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPAuthnFilter.java b/opendj3/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPAuthnFilter.java
new file mode 100644
index 0000000..6b3d787
--- /dev/null
+++ b/opendj3/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPAuthnFilter.java
@@ -0,0 +1,393 @@
+/*
+ * 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.servlet;
+
+import static org.forgerock.json.resource.SecurityContext.AUTHZID_DN;
+import static org.forgerock.json.resource.SecurityContext.AUTHZID_ID;
+import static org.forgerock.json.resource.servlet.SecurityContextFactory.ATTRIBUTE_AUTHCID;
+import static org.forgerock.json.resource.servlet.SecurityContextFactory.ATTRIBUTE_AUTHZID;
+import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
+import static org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPContextFactory.ATTRIBUTE_AUTHN_CONNECTION;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.codehaus.jackson.JsonParser;
+import org.codehaus.jackson.map.ObjectMapper;
+import org.forgerock.json.fluent.JsonValue;
+import org.forgerock.json.fluent.JsonValueException;
+import org.forgerock.json.resource.ResourceException;
+import org.forgerock.json.resource.servlet.CompletionHandler;
+import org.forgerock.json.resource.servlet.CompletionHandlerFactory;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.ErrorResultException;
+import org.forgerock.opendj.ldap.ResultHandler;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.responses.BindResult;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.rest2ldap.Rest2LDAP;
+
+/**
+ * An LDAP based authentication Servlet filter.
+ * <p>
+ * TODO: this is a work in progress. In particular, in order to embed this into
+ * the OpenDJ HTTP listener it will need to provide a configuration API.
+ */
+public final class Rest2LDAPAuthnFilter implements Filter {
+    /** Indicates how authentication should be performed. */
+    private static enum AuthenticationMethod {
+        SASL_PLAIN, SEARCH_SIMPLE, SIMPLE;
+    }
+
+    private static final String INIT_PARAM_CONFIG_FILE = "config-file";
+    private static final ObjectMapper JSON_MAPPER = new ObjectMapper().configure(
+            JsonParser.Feature.ALLOW_COMMENTS, true);
+
+    /** Indicates whether or not authentication should be performed. */
+    private boolean isEnabled = false;
+    private String altAuthenticationPasswordHeader;
+    private String altAuthenticationUsernameHeader;
+    private AuthenticationMethod authenticationMethod = AuthenticationMethod.SEARCH_SIMPLE;
+    private ConnectionFactory bindLDAPConnectionFactory;
+    private boolean reuseAuthenticatedConnection = true;
+    private String saslAuthzIdTemplate;
+    private final Schema schema = Schema.getDefaultSchema();
+    private DN searchBaseDN;
+    private String searchFilterTemplate;
+    private ConnectionFactory searchLDAPConnectionFactory;
+    private SearchScope searchScope = SearchScope.WHOLE_SUBTREE;
+    private boolean supportAltAuthentication;
+    private boolean supportHTTPBasicAuthentication = true;
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void destroy() {
+        // TODO: We should release any resources maintained by the filter, such as connection pools.
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void doFilter(final ServletRequest request, final ServletResponse response,
+            final FilterChain chain) throws IOException, ServletException {
+        // Skip this filter if authentication has not been configured.
+        if (!isEnabled) {
+            chain.doFilter(request, response);
+            return;
+        }
+
+        // First of all parse the HTTP headers for authentication credentials.
+        if (!(request instanceof HttpServletRequest && response instanceof HttpServletResponse)) {
+            // This should never happen.
+            throw new ServletException("non-HTTP request or response");
+        }
+
+        // TODO: support logout, sessions, reauth?
+        final HttpServletRequest req = (HttpServletRequest) request;
+        final HttpServletResponse res = (HttpServletResponse) response;
+
+        /*
+         * Store the authenticated connection so that it can be re-used by the
+         * servlet if needed. However, make sure that it is closed on
+         * completion.
+         */
+        final AtomicReference<Connection> savedConnection = new AtomicReference<Connection>();
+        final CompletionHandler completionHandler =
+                CompletionHandlerFactory.getInstance(req.getServletContext())
+                        .createCompletionHandler(req, res);
+        if (completionHandler.isAsynchronous()) {
+            completionHandler.addCompletionListener(new Runnable() {
+                @Override
+                public void run() {
+                    closeConnection(savedConnection);
+                }
+            });
+        }
+
+        try {
+            final String headerUsername =
+                    supportAltAuthentication ? req.getHeader(altAuthenticationUsernameHeader)
+                            : null;
+            final String headerPassword =
+                    supportAltAuthentication ? req.getHeader(altAuthenticationPasswordHeader)
+                            : null;
+            final String headerAuthorization =
+                    supportHTTPBasicAuthentication ? req.getHeader("Authorization") : null;
+
+            final String username;
+            final char[] password;
+            if (headerUsername != null) {
+                if (headerPassword == null || headerUsername.isEmpty() || headerPassword.isEmpty()) {
+                    throw ResourceException.getException(401);
+                }
+                username = headerUsername;
+                password = headerPassword.toCharArray();
+            } else if (headerAuthorization != null) {
+                final StringTokenizer st = new StringTokenizer(headerAuthorization);
+                final String method = st.nextToken();
+                if (method == null || !method.equalsIgnoreCase(HttpServletRequest.BASIC_AUTH)) {
+                    throw ResourceException.getException(401);
+                }
+                final String b64Credentials = st.nextToken();
+                if (b64Credentials == null) {
+                    throw ResourceException.getException(401);
+                }
+                final String credentials = ByteString.valueOfBase64(b64Credentials).toString();
+                final String[] usernameAndPassword = credentials.split(":");
+                if (usernameAndPassword.length != 2) {
+                    throw ResourceException.getException(401);
+                }
+                username = usernameAndPassword[0];
+                password = usernameAndPassword[1].toCharArray();
+            } else {
+                throw ResourceException.getException(401);
+            }
+
+            // If we've got here then we have a username and password.
+            switch (authenticationMethod) {
+            case SIMPLE:
+                bindLDAPConnectionFactory.getConnectionAsync(new ResultHandler<Connection>() {
+                    @Override
+                    public void handleErrorResult(ErrorResultException error) {
+                        completionHandler.onError(asResourceException(error));
+                    }
+
+                    @Override
+                    public void handleResult(final Connection connection) {
+                        savedConnection.set(connection);
+                        connection.bindAsync(Requests.newSimpleBindRequest(username, password),
+                                null, new ResultHandler<BindResult>() {
+
+                                    @Override
+                                    public void handleErrorResult(ErrorResultException error) {
+                                        completionHandler.onError(asResourceException(error));
+                                    }
+
+                                    @Override
+                                    public void handleResult(BindResult result) {
+                                        // Cache the pre-authenticated connection.
+                                        if (reuseAuthenticatedConnection) {
+                                            req.setAttribute(ATTRIBUTE_AUTHN_CONNECTION, connection);
+                                        }
+
+                                        // Pass through the authentication ID.
+                                        req.setAttribute(ATTRIBUTE_AUTHCID, username);
+
+                                        // Pass through authorization information.
+                                        final Map<String, Object> authzid =
+                                                new LinkedHashMap<String, Object>(2);
+                                        authzid.put(AUTHZID_DN, username);
+                                        authzid.put(AUTHZID_ID, username);
+                                        req.setAttribute(ATTRIBUTE_AUTHZID, authzid);
+
+                                        // Invoke the remained of the filter chain.
+                                        try {
+                                            chain.doFilter(request, response);
+                                        } catch (Throwable t) {
+                                            completionHandler.onError(asResourceException(t));
+                                        }
+                                    }
+                                });
+                    }
+                });
+                break;
+            case SASL_PLAIN:
+            case SEARCH_SIMPLE:
+                throw ResourceException.getException(401);
+            }
+
+            /*
+             * Block until authentication completes if needed and then invoke
+             * the remainder of the filter chain.
+             */
+            if (!completionHandler.isAsynchronous()) {
+                completionHandler.awaitIfNeeded();
+                chain.doFilter(request, response);
+                closeConnection(savedConnection);
+            }
+        } catch (final Throwable t) {
+            // Complete and close the connection if needed.
+            completionHandler.onError(t);
+            if (!completionHandler.isAsynchronous()) {
+                closeConnection(savedConnection);
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void init(final FilterConfig config) throws ServletException {
+        // FIXME: make it possible to configure the filter externally, especially
+        // connection factories.
+        final String configFileName = config.getInitParameter(INIT_PARAM_CONFIG_FILE);
+        if (configFileName == null) {
+            throw new ServletException("Authentication filter initialization parameter '"
+                    + INIT_PARAM_CONFIG_FILE + "' not specified");
+        }
+        final InputStream configFile =
+                config.getServletContext().getResourceAsStream(configFileName);
+        if (configFile == null) {
+            throw new ServletException("Servlet filter configuration file '" + configFileName
+                    + "' not found");
+        }
+        try {
+            // Parse the config file.
+            final Object content = JSON_MAPPER.readValue(configFile, Object.class);
+            if (!(content instanceof Map)) {
+                throw new ServletException("Servlet filter configuration file '" + configFileName
+                        + "' does not contain a valid JSON configuration");
+            }
+
+            // Parse the authentication configuration.
+            final JsonValue configuration = new JsonValue(content);
+            final JsonValue authnConfig = configuration.get("authenticationFilter");
+            if (!authnConfig.isNull()) {
+                supportHTTPBasicAuthentication =
+                        authnConfig.get("supportHTTPBasicAuthentication").required().asBoolean();
+
+                // Alternative HTTP authentication.
+                supportAltAuthentication =
+                        authnConfig.get("supportAltAuthentication").required().asBoolean();
+                if (supportAltAuthentication) {
+                    altAuthenticationUsernameHeader =
+                            authnConfig.get("altAuthenticationUsernameHeader").required()
+                                    .asString();
+                    altAuthenticationPasswordHeader =
+                            authnConfig.get("altAuthenticationPasswordHeader").required()
+                                    .asString();
+                }
+
+                // Should the authenticated connection should be cached for use by subsequent LDAP operations?
+                reuseAuthenticatedConnection =
+                        authnConfig.get("reuseAuthenticatedConnection").required().asBoolean();
+
+                // Parse the authentication method and associated parameters.
+                authenticationMethod = parseAuthenticationMethod(authnConfig);
+                switch (authenticationMethod) {
+                case SIMPLE:
+                    // Nothing to do.
+                    break;
+                case SASL_PLAIN:
+                    saslAuthzIdTemplate =
+                            authnConfig.get("saslAuthzIdTemplate").required().asString();
+                    break;
+                case SEARCH_SIMPLE:
+                    searchBaseDN =
+                            DN.valueOf(authnConfig.get("searchBaseDN").required().asString(),
+                                    schema);
+                    searchScope = parseSearchScope(authnConfig);
+                    searchFilterTemplate =
+                            authnConfig.get("searchFilterTemplate").required().asString();
+
+                    // Parse the LDAP connection factory to be used for searches.
+                    final String ldapFactoryName =
+                            authnConfig.get("searchLDAPConnectionFactory").required().asString();
+                    searchLDAPConnectionFactory =
+                            Rest2LDAP.configureConnectionFactory(configuration.get(
+                                    "ldapConnectionFactories").required(), ldapFactoryName);
+                    break;
+                }
+
+                // Parse the LDAP connection factory to be used for binds.
+                final String ldapFactoryName =
+                        authnConfig.get("bindLDAPConnectionFactory").required().asString();
+                bindLDAPConnectionFactory =
+                        Rest2LDAP.configureConnectionFactory(configuration.get(
+                                "ldapConnectionFactories").required(), ldapFactoryName);
+
+                isEnabled = true;
+            }
+        } catch (final ServletException e) {
+            // Rethrow.
+            throw e;
+        } catch (final Exception e) {
+            throw new ServletException("Servlet filter configuration file '" + configFileName
+                    + "' could not be read: " + e.getMessage());
+        } finally {
+            try {
+                configFile.close();
+            } catch (final Exception e) {
+                // Ignore.
+            }
+        }
+    }
+
+    private AuthenticationMethod parseAuthenticationMethod(final JsonValue configuration) {
+        if (configuration.isDefined("method")) {
+            final String method = configuration.get("method").asString();
+            if (method.equalsIgnoreCase("simple")) {
+                return AuthenticationMethod.SIMPLE;
+            } else if (method.equalsIgnoreCase("sasl-plain")) {
+                return AuthenticationMethod.SASL_PLAIN;
+            } else if (method.equalsIgnoreCase("search-simple")) {
+                return AuthenticationMethod.SEARCH_SIMPLE;
+            } else {
+                throw new JsonValueException(configuration,
+                        "Illegal authentication method: must be either 'simple', "
+                                + "'sasl-plain', or 'search-simple'");
+            }
+        } else {
+            return AuthenticationMethod.SEARCH_SIMPLE;
+        }
+    }
+
+    private SearchScope parseSearchScope(final JsonValue configuration) {
+        if (configuration.isDefined("searchScope")) {
+            final String scope = configuration.get("searchScope").asString();
+            if (scope.equalsIgnoreCase("sub")) {
+                return SearchScope.WHOLE_SUBTREE;
+            } else if (scope.equalsIgnoreCase("one")) {
+                return SearchScope.SINGLE_LEVEL;
+            } else {
+                throw new JsonValueException(configuration,
+                        "Illegal search scope: must be either 'sub' or 'one'");
+            }
+        } else {
+            return SearchScope.WHOLE_SUBTREE;
+        }
+    }
+
+    private void closeConnection(final AtomicReference<Connection> savedConnection) {
+        final Connection connection = savedConnection.get();
+        if (connection != null) {
+            connection.close();
+        }
+    }
+
+}
diff --git a/opendj3/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPConnectionFactoryProvider.java b/opendj3/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPConnectionFactoryProvider.java
index 782bacd..276349b 100644
--- a/opendj3/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPConnectionFactoryProvider.java
+++ b/opendj3/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPConnectionFactoryProvider.java
@@ -102,7 +102,7 @@
             for (final String mappingUrl : mappings.keys()) {
                 final JsonValue mapping = mappings.get(mappingUrl);
                 final CollectionResourceProvider provider =
-                        Rest2LDAP.builder().connectionFactory(ldapFactory).authorizationPolicy(
+                        Rest2LDAP.builder().ldapConnectionFactory(ldapFactory).authorizationPolicy(
                                 authzPolicy).proxyAuthzIdTemplate(proxyAuthzTemplate)
                                 .configureMapping(mapping).build();
                 router.addRoute(mappingUrl, provider);
diff --git a/opendj3/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPContextFactory.java b/opendj3/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPContextFactory.java
index 330bfb4..0ef4e4d 100644
--- a/opendj3/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPContextFactory.java
+++ b/opendj3/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPContextFactory.java
@@ -170,9 +170,7 @@
      */
     @Override
     public Context createContext(final HttpServletRequest request) throws ResourceException {
-        final Context securityContext =
-                SecurityContextFactory.getHttpServletContextFactory().createContext(request);
-        return createContext(securityContext, request);
+        return createContext(new RootContext(), request);
     }
 
 }
diff --git a/opendj3/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/web.xml b/opendj3/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/web.xml
index afc4e7f..2ba6b1c 100644
--- a/opendj3/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/web.xml
+++ b/opendj3/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/web.xml
@@ -18,12 +18,32 @@
             <param-name>config-file</param-name>
             <param-value>/opendj-rest2ldap-servlet.json</param-value>
         </init-param>
+        
+         <init-param>
+            <param-name>context-factory-class</param-name>
+            <param-value>org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPContextFactory</param-value>
+        </init-param>
     </servlet>
-
+    
     <servlet-mapping>
         <servlet-name>OpenDJ Commons REST LDAP Gateway</servlet-name>
         <url-pattern>/*</url-pattern>
     </servlet-mapping>
 
+    <filter>
+        <filter-name>OpenDJ Commons REST LDAP Authentication Filter</filter-name>
+        <filter-class>org.forgerock.opendj.rest2ldap.servlet.Rest2LDAPAuthnFilter</filter-class>
+
+        <init-param>
+            <param-name>config-file</param-name>
+            <param-value>/opendj-rest2ldap-servlet.json</param-value>
+        </init-param>
+    </filter>
+
+    <filter-mapping>
+        <filter-name>OpenDJ Commons REST LDAP Authentication Filter</filter-name>
+        <url-pattern>/*</url-pattern>
+    </filter-mapping>
+
 </web-app>
  
\ No newline at end of file
diff --git a/opendj3/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json b/opendj3/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json
index 14645cc..cef1345 100644
--- a/opendj3/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json
+++ b/opendj3/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json
@@ -69,7 +69,7 @@
         // "sasl-plain"    - the username is an authzid which will be
         //                   substituted into the "saslAuthzIdTemplate" using
         //                   %s substitution
-        // "search+simple" - the user's DN will be resolved by performing an
+        // "search-simple" - the user's DN will be resolved by performing an
         //                   LDAP search using a filter constructed by
         //                   substituting the username into the
         //                   "searchFilterTemplate" using %s substitution.
@@ -84,11 +84,11 @@
         "saslAuthzIdTemplate" : "dn:uid=%s,ou=people,dc=example,dc=com",
         
         // The connection factory which will be used for performing LDAP
-        // searches to locate users when "search+simple" authentication is
+        // searches to locate users when "search-simple" authentication is
         // enabled.
         "searchLDAPConnectionFactory" : "root",
         
-        // The search parameters to use for "search+simple" authentication.
+        // The search parameters to use for "search-simple" authentication.
         "searchBaseDN"         : "ou=people,dc=example,dc=com",
         "searchScope"          : "sub", // Or "one".
         "searchFilterTemplate" : "(&(objectClass=inetOrgPerson)(uid=%s))"
@@ -120,7 +120,7 @@
         //                 derived from the "proxyAuthzIdTemplate". Proxied
         //                 authorization will only be used if there is no
         //                 pre-authenticated connection available.
-        "authorizationPolicy" : "none",
+        "authorizationPolicy" : "proxy",
         
         // The AuthzID template which will be used for proxied authorization.
         // The template should contain fields which are expected to be found in
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Context.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Context.java
index f6c9579..ba92a5b 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Context.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Context.java
@@ -16,7 +16,7 @@
 package org.forgerock.opendj.rest2ldap;
 
 import static org.forgerock.opendj.ldap.ErrorResultException.newErrorResult;
-import static org.forgerock.opendj.rest2ldap.Utils.adapt;
+import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
 
 import java.io.Closeable;
 import java.util.LinkedHashMap;
@@ -302,7 +302,7 @@
             config.connectionFactory().getConnectionAsync(new ResultHandler<Connection>() {
                 @Override
                 public final void handleErrorResult(final ErrorResultException error) {
-                    handler.handleError(adapt(error));
+                    handler.handleError(asResourceException(error));
                 }
 
                 @Override
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
index b4eda51..ace3b59 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
@@ -18,8 +18,8 @@
 import static org.forgerock.opendj.ldap.Filter.alwaysFalse;
 import static org.forgerock.opendj.ldap.Filter.alwaysTrue;
 import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.CONTROLS;
+import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
 import static org.forgerock.opendj.rest2ldap.Utils.accumulate;
-import static org.forgerock.opendj.rest2ldap.Utils.adapt;
 import static org.forgerock.opendj.rest2ldap.Utils.toFilter;
 import static org.forgerock.opendj.rest2ldap.Utils.transform;
 
@@ -251,7 +251,7 @@
 
                                 @Override
                                 public void handleErrorResult(final ErrorResultException error) {
-                                    pendingResult.compareAndSet(null, adapt(error));
+                                    pendingResult.compareAndSet(null, asResourceException(error));
                                     completeIfNecessary();
                                 }
 
@@ -312,7 +312,7 @@
                         new org.forgerock.opendj.ldap.ResultHandler<SearchResultEntry>() {
                             @Override
                             public void handleErrorResult(final ErrorResultException error) {
-                                h.handleError(adapt(error));
+                                h.handleError(asResourceException(error));
                             }
 
                             @Override
@@ -559,7 +559,7 @@
         return new org.forgerock.opendj.ldap.ResultHandler<Result>() {
             @Override
             public void handleErrorResult(final ErrorResultException error) {
-                handler.handleError(adapt(error));
+                handler.handleError(asResourceException(error));
             }
 
             @Override
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
index ae72b5f..51b3a09 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
@@ -17,8 +17,8 @@
 
 import static java.util.Collections.singletonList;
 import static org.forgerock.opendj.ldap.ErrorResultException.newErrorResult;
+import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
 import static org.forgerock.opendj.rest2ldap.Utils.accumulate;
-import static org.forgerock.opendj.rest2ldap.Utils.adapt;
 import static org.forgerock.opendj.rest2ldap.Utils.ensureNotNull;
 import static org.forgerock.opendj.rest2ldap.Utils.transform;
 import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_WRITE;
@@ -199,7 +199,7 @@
 
                             @Override
                             public void handleErrorResult(final ErrorResultException error) {
-                                h.handleError(adapt(error)); // Propagate.
+                                h.handleError(asResourceException(error)); // Propagate.
                             }
 
                             @Override
@@ -234,7 +234,7 @@
                 readEntry(c, dn, h);
             } catch (final Exception ex) {
                 // The LDAP attribute could not be decoded.
-                h.handleError(adapt(ex));
+                h.handleError(asResourceException(ex));
             }
         } else {
             try {
@@ -264,7 +264,7 @@
                 }
             } catch (final Exception ex) {
                 // The LDAP attribute could not be decoded.
-                h.handleError(adapt(ex));
+                h.handleError(asResourceException(ex));
             }
         }
     }
@@ -373,7 +373,7 @@
                                                                                         .toString()
                                                                                 + "' is ambiguous");
                                                     } catch (ErrorResultException e) {
-                                                        re = adapt(e);
+                                                        re = asResourceException(e);
                                                     }
                                                     exception.compareAndSet(null, re);
                                                     completeIfNecessary();
@@ -433,7 +433,7 @@
                     @Override
                     public void handleErrorResult(final ErrorResultException error) {
                         if (!(error instanceof EntryNotFoundException)) {
-                            handler.handleError(adapt(error));
+                            handler.handleError(asResourceException(error));
                         } else {
                             // The referenced entry does not exist so ignore it since it cannot be mapped.
                             handler.handleResult(null);
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
index 9a7bed6..2ad567f 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
@@ -35,20 +35,29 @@
 import org.forgerock.json.resource.BadRequestException;
 import org.forgerock.json.resource.CollectionResourceProvider;
 import org.forgerock.json.resource.ResourceException;
+import org.forgerock.opendj.ldap.AssertionFailureException;
 import org.forgerock.opendj.ldap.Attribute;
 import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.AuthenticationException;
+import org.forgerock.opendj.ldap.AuthorizationException;
 import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.ConnectionException;
 import org.forgerock.opendj.ldap.ConnectionFactory;
 import org.forgerock.opendj.ldap.Connections;
 import org.forgerock.opendj.ldap.DN;
 import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.EntryNotFoundException;
+import org.forgerock.opendj.ldap.ErrorResultException;
 import org.forgerock.opendj.ldap.FailoverLoadBalancingAlgorithm;
 import org.forgerock.opendj.ldap.Filter;
 import org.forgerock.opendj.ldap.LDAPConnectionFactory;
 import org.forgerock.opendj.ldap.LinkedAttribute;
+import org.forgerock.opendj.ldap.MultipleEntriesFoundException;
 import org.forgerock.opendj.ldap.RDN;
+import org.forgerock.opendj.ldap.ResultCode;
 import org.forgerock.opendj.ldap.RoundRobinLoadBalancingAlgorithm;
 import org.forgerock.opendj.ldap.SearchScope;
+import org.forgerock.opendj.ldap.TimeoutResultException;
 import org.forgerock.opendj.ldap.requests.BindRequest;
 import org.forgerock.opendj.ldap.requests.Requests;
 import org.forgerock.opendj.ldap.requests.SearchRequest;
@@ -67,11 +76,11 @@
      */
     public static final class Builder {
         private final List<Attribute> additionalLDAPAttributes = new LinkedList<Attribute>();
+        private AuthorizationPolicy authzPolicy = AuthorizationPolicy.NONE;
         private DN baseDN; // TODO: support template variables.
         private ConnectionFactory factory;
         private MVCCStrategy mvccStrategy;
         private NameStrategy nameStrategy;
-        private AuthorizationPolicy authzPolicy = AuthorizationPolicy.NONE;
         private AuthzIdTemplate proxiedAuthzTemplate;
         private ReadOnUpdatePolicy readOnUpdatePolicy = CONTROLS;
         private AttributeMapper rootMapper;
@@ -91,6 +100,11 @@
             return additionalLDAPAttribute(new LinkedAttribute(ad(attribute), values));
         }
 
+        public Builder authorizationPolicy(final AuthorizationPolicy policy) {
+            this.authzPolicy = ensureNotNull(policy);
+            return this;
+        }
+
         public Builder baseDN(final DN dn) {
             ensureNotNull(dn);
             this.baseDN = dn;
@@ -135,74 +149,9 @@
         /**
          * Configures the JSON to LDAP mapping using the provided JSON
          * configuration. The caller is still required to set the connection
-         * factory. The configuration should look like this, excluding the
-         * C-like comments:
-         *
-         * <pre>
-         * {
-         *     // The base DN beneath which LDAP entries are to be found.
-         *     "baseDN" : "ou=people,dc=example,dc=com",
-         *
-         *     // The mechanism which should be used for read resources during updates, must be
-         *     // one of "disabled", "controls", or "search".
-         *     "readOnUpdatePolicy" : "controls",
-         *
-         *     // Additional LDAP attributes which should be included with entries during add (create) operations.
-         *     "additionalLDAPAttributes" : [
-         *         {
-         *             "type" : "objectClass",
-         *             "values" : [
-         *                 "top",
-         *                 "person"
-         *             ]
-         *         }
-         *     ],
-         *
-         *     // The strategy which should be used for deriving LDAP entry names from JSON resources.
-         *     "namingStrategy" : {
-         *         // Option 1) the RDN and resource ID are both derived from a single user attribute in the entry.
-         *         "strategy" : "clientDNNaming",
-         *         "dnAttribute" : "uid"
-         *
-         *         // Option 2) the RDN and resource ID are derived from separate user attributes in the entry.
-         *         "strategy" : "clientNaming",
-         *         "dnAttribute" : "uid",
-         *         "idAttribute" : "mail"
-         *
-         *         // Option 3) the RDN and is derived from a user attribute and the resource ID from an operational
-         *         //           attribute in the entry.
-         *         "strategy" : "serverNaming",
-         *         "dnAttribute" : "uid",
-         *         "idAttribute" : "entryUUID"
-         *     },
-         *
-         *     // The attribute which will be used for performing MVCC.
-         *     "etagAttribute" : "etag",
-         *
-         *     // The JSON to LDAP attribute mappings.
-         *     "attributes" : {
-         *         "schemas"     : { "constant" : [ "urn:scim:schemas:core:1.0" ] },
-         *         "id"          : { "simple"   : { "ldapAttribute" : "uid", "isSingleValued" : true, "isRequired" : true, "writability" : "createOnly" } },
-         *         "rev"         : { "simple"   : { "ldapAttribute" : "etag", "isSingleValued" : true, "writability" : "readOnly" } },
-         *         "userName"    : { "simple"   : { "ldapAttribute" : "mail", "isSingleValued" : true, "writability" : "readOnly" } },
-         *         "displayName" : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true, "isRequired" : true } },
-         *         "name"        : { "object"   : {
-         *             "givenName"  : { "simple"   : { "ldapAttribute" : "givenName", "isSingleValued" : true } },
-         *             "familyName" : { "simple"   : { "ldapAttribute" : "sn", "isSingleValued" : true, "isRequired" : true } },
-         *         },
-         *         "manager"     : { "reference" : {
-         *             "ldapAttribute" : "manager",
-         *             "baseDN"        : "ou=people,dc=example,dc=com",
-         *             "mapper"        : { "object" : {
-         *                 "id"          : { "simple"   : { "ldapAttribute" : "uid", "isSingleValued" : true } },
-         *                 "displayName" : { "simple"   : { "ldapAttribute" : "cn", "isSingleValued" : true } }
-         *             } }
-         *         },
-         *         ...
-         *     }
-         * }
-         * </pre>
-         *
+         * factory. See the sample configuration file for a detailed description
+         * of its content.
+         * 
          * @param configuration
          *            The JSON configuration.
          * @return A reference to this builder.
@@ -250,7 +199,7 @@
             return this;
         }
 
-        public Builder connectionFactory(final ConnectionFactory factory) {
+        public Builder ldapConnectionFactory(final ConnectionFactory factory) {
             this.factory = factory;
             return this;
         }
@@ -260,10 +209,15 @@
             return this;
         }
 
+        public Builder proxyAuthzIdTemplate(final String template) {
+            this.proxiedAuthzTemplate = template != null ? new AuthzIdTemplate(template) : null;
+            return this;
+        }
+
         /**
          * Sets the policy which should be used in order to read an entry before
          * it is deleted, or after it is added or modified.
-         *
+         * 
          * @param policy
          *            The policy which should be used in order to read an entry
          *            before it is deleted, or after it is added or modified.
@@ -277,7 +231,7 @@
         /**
          * Sets the schema which should be used when attribute types and
          * controls.
-         *
+         * 
          * @param schema
          *            The schema which should be used when attribute types and
          *            controls.
@@ -320,16 +274,6 @@
             return useEtagAttribute(ad(attribute));
         }
 
-        public Builder authorizationPolicy(final AuthorizationPolicy policy) {
-            this.authzPolicy = ensureNotNull(policy);
-            return this;
-        }
-
-        public Builder proxyAuthzIdTemplate(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()));
@@ -551,61 +495,60 @@
 
     }
 
+    /**
+     * Adapts a {@code Throwable} to a {@code ResourceException}. If the
+     * {@code Throwable} is an LDAP {@code ErrorResultException} then an
+     * appropriate {@code ResourceException} is returned, otherwise an
+     * {@code InternalServerErrorException} is returned.
+     * 
+     * @param t
+     *            The {@code Throwable} to be converted.
+     * @return The equivalent resource exception.
+     */
+    public static ResourceException asResourceException(final Throwable t) {
+        int resourceResultCode;
+        try {
+            throw t;
+        } catch (final ResourceException e) {
+            return e;
+        } catch (final AssertionFailureException e) {
+            resourceResultCode = ResourceException.VERSION_MISMATCH;
+        } catch (final AuthenticationException e) {
+            resourceResultCode = 401;
+        } catch (final AuthorizationException e) {
+            resourceResultCode = ResourceException.FORBIDDEN;
+        } catch (final ConnectionException e) {
+            resourceResultCode = ResourceException.UNAVAILABLE;
+        } catch (final EntryNotFoundException e) {
+            resourceResultCode = ResourceException.NOT_FOUND;
+        } catch (final MultipleEntriesFoundException e) {
+            resourceResultCode = ResourceException.INTERNAL_ERROR;
+        } catch (final TimeoutResultException e) {
+            resourceResultCode = 408;
+        } catch (final ErrorResultException e) {
+            final ResultCode rc = e.getResult().getResultCode();
+            if (rc.equals(ResultCode.ADMIN_LIMIT_EXCEEDED)) {
+                resourceResultCode = 413; // Request Entity Too Large
+            } else if (rc.equals(ResultCode.SIZE_LIMIT_EXCEEDED)) {
+                resourceResultCode = 413; // Request Entity Too Large
+            } else {
+                resourceResultCode = ResourceException.INTERNAL_ERROR;
+            }
+        } catch (final Throwable tmp) {
+            resourceResultCode = ResourceException.INTERNAL_ERROR;
+        }
+        return ResourceException.getException(resourceResultCode, t.getMessage(), t);
+    }
+
     public static Builder builder() {
         return new Builder();
     }
 
     /**
      * 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" : [
-     *             {
-     *                 "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
-     *             },
-     *         ],
-     *
-     *         // Connection pool configuration.
-     *         "connectionPoolSize"       : 10,
-     *         "heartBeatIntervalSeconds" : 30
-     *     },
-     *
-     *     // The same pool of servers except authenticated as cn=directory manager.
-     *     "root" : {
-     *         "inheritFrom"    : "default",
-     *         "authentication" : {
-     *             "simple" : {
-     *                 "bindDN"       : "cn=directory manager",
-     *                 "bindPassword" : "password"
-     *             }
-     *         }
-     *     }
-     * }
-     * </pre>
-     *
+     * provided JSON list of factory configurations. See the sample
+     * configuration file for a detailed description of its content.
+     * 
      * @param configuration
      *            The JSON configuration.
      * @param name
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
index 2297aae..4706fbe 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java
@@ -21,7 +21,7 @@
 import static java.util.Collections.singletonList;
 import static org.forgerock.opendj.ldap.Filter.alwaysFalse;
 import static org.forgerock.opendj.ldap.Functions.fixedFunction;
-import static org.forgerock.opendj.rest2ldap.Utils.adapt;
+import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
 import static org.forgerock.opendj.rest2ldap.Utils.base64ToByteString;
 import static org.forgerock.opendj.rest2ldap.Utils.byteStringToBase64;
 import static org.forgerock.opendj.rest2ldap.Utils.byteStringToJson;
@@ -183,7 +183,8 @@
             final String operator, final Object valueAssertion, final ResultHandler<Filter> h) {
         if (jsonAttribute.isEmpty()) {
             try {
-                final ByteString va = valueAssertion != null ? encoder().apply(valueAssertion, null) : null;
+                final ByteString va =
+                        valueAssertion != null ? encoder().apply(valueAssertion, null) : null;
                 h.handleResult(toFilter(c, type, ldapAttributeName.toString(), va));
             } catch (Exception e) {
                 // Invalid assertion value - bad request.
@@ -212,7 +213,7 @@
             h.handleResult(value != null ? new JsonValue(value) : null);
         } catch (Exception ex) {
             // The LDAP attribute could not be decoded.
-            h.handleError(adapt(ex));
+            h.handleError(asResourceException(ex));
         }
     }
 
diff --git a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
index f8f5c14..c2d87d2 100644
--- a/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
+++ b/opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java
@@ -26,6 +26,7 @@
 import static org.forgerock.opendj.ldap.schema.CoreSchema.getBooleanSyntax;
 import static org.forgerock.opendj.ldap.schema.CoreSchema.getGeneralizedTimeSyntax;
 import static org.forgerock.opendj.ldap.schema.CoreSchema.getIntegerSyntax;
+import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -36,22 +37,13 @@
 
 import org.forgerock.json.resource.ResourceException;
 import org.forgerock.json.resource.ResultHandler;
-import org.forgerock.opendj.ldap.AssertionFailureException;
 import org.forgerock.opendj.ldap.Attribute;
 import org.forgerock.opendj.ldap.AttributeDescription;
-import org.forgerock.opendj.ldap.AuthenticationException;
-import org.forgerock.opendj.ldap.AuthorizationException;
 import org.forgerock.opendj.ldap.ByteString;
-import org.forgerock.opendj.ldap.ConnectionException;
-import org.forgerock.opendj.ldap.EntryNotFoundException;
-import org.forgerock.opendj.ldap.ErrorResultException;
 import org.forgerock.opendj.ldap.Filter;
 import org.forgerock.opendj.ldap.Function;
 import org.forgerock.opendj.ldap.GeneralizedTime;
 import org.forgerock.opendj.ldap.LinkedAttribute;
-import org.forgerock.opendj.ldap.MultipleEntriesFoundException;
-import org.forgerock.opendj.ldap.ResultCode;
-import org.forgerock.opendj.ldap.TimeoutResultException;
 import org.forgerock.opendj.ldap.schema.Syntax;
 
 /**
@@ -187,51 +179,6 @@
         return new AccumulatingResultHandler<V>(size, handler);
     }
 
-    /**
-     * Adapts a {@code Throwable} to a {@code ResourceException}. If the
-     * {@code Throwable} is an LDAP {@code ErrorResultException} then an
-     * appropriate {@code ResourceException} is returned, otherwise an
-     * {@code InternalServerErrorException} is returned.
-     *
-     * @param t
-     *            The {@code Throwable} to be converted.
-     * @return The equivalent resource exception.
-     */
-    static ResourceException adapt(final Throwable t) {
-        int resourceResultCode;
-        try {
-            throw t;
-        } catch (final ResourceException e) {
-            return e;
-        } catch (final AssertionFailureException e) {
-            resourceResultCode = ResourceException.VERSION_MISMATCH;
-        } catch (final AuthenticationException e) {
-            resourceResultCode = 401;
-        } catch (final AuthorizationException e) {
-            resourceResultCode = ResourceException.FORBIDDEN;
-        } catch (final ConnectionException e) {
-            resourceResultCode = ResourceException.UNAVAILABLE;
-        } catch (final EntryNotFoundException e) {
-            resourceResultCode = ResourceException.NOT_FOUND;
-        } catch (final MultipleEntriesFoundException e) {
-            resourceResultCode = ResourceException.INTERNAL_ERROR;
-        } catch (final TimeoutResultException e) {
-            resourceResultCode = 408;
-        } catch (final ErrorResultException e) {
-            final ResultCode rc = e.getResult().getResultCode();
-            if (rc.equals(ResultCode.ADMIN_LIMIT_EXCEEDED)) {
-                resourceResultCode = 413; // Request Entity Too Large
-            } else if (rc.equals(ResultCode.SIZE_LIMIT_EXCEEDED)) {
-                resourceResultCode = 413; // Request Entity Too Large
-            } else {
-                resourceResultCode = ResourceException.INTERNAL_ERROR;
-            }
-        } catch (final Throwable tmp) {
-            resourceResultCode = ResourceException.INTERNAL_ERROR;
-        }
-        return ResourceException.getException(resourceResultCode, t.getMessage(), t);
-    }
-
     static Object attributeToJson(final Attribute a) {
         final Function<ByteString, Object, Void> f =
                 fixedFunction(BYTESTRING_TO_JSON, a.getAttributeDescription());
@@ -376,7 +323,7 @@
                 try {
                     handler.handleResult(f.apply(result, null));
                 } catch (Throwable t) {
-                    handler.handleError(adapt(t));
+                    handler.handleError(asResourceException(t));
                 }
             }
         };

--
Gitblit v1.10.0