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

Matthew Swift
23.46.2013 19536840ae2c706f4ad9ff4742e52dfc5f6ebea7
Partial fix for OPENDJ-694: Implement HTTP BASIC authentication

* json-resource-servlet - refactor Servlet 2.x / 3.x abstraction layer so that it can be reused in the LDAP authentication filter
* opendj-rest2ldap-servlet - add preliminary support for performing LDAP simple bind based authentication.
1 files added
10 files modified
722 ■■■■ changed files
opendj3/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPAuthnFilter.java 393 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPConnectionFactoryProvider.java 2 ●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPContextFactory.java 4 ●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap-servlet/src/main/webapp/WEB-INF/web.xml 22 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap-servlet/src/main/webapp/opendj-rest2ldap-servlet.json 8 ●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Context.java 4 ●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java 8 ●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java 12 ●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java 205 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/SimpleAttributeMapper.java 7 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Utils.java 57 ●●●●● patch | view | raw | blame | history
opendj3/opendj-rest2ldap-servlet/src/main/java/org/forgerock/opendj/rest2ldap/servlet/Rest2LDAPAuthnFilter.java
New file
@@ -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();
        }
    }
}
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);
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);
    }
}
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>
 
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
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
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
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);
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
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));
        }
    }
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));
                }
            }
        };